diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 15024e3..ea1f44d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -5,12 +5,18 @@ on: branches: - master +permissions: {} + jobs: build: runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: diff --git a/.github/workflows/pr-actions.yaml b/.github/workflows/pr-actions.yaml new file mode 100644 index 0000000..af5b328 --- /dev/null +++ b/.github/workflows/pr-actions.yaml @@ -0,0 +1,27 @@ +name: pr-actions + +permissions: {} + +on: + pull_request: + branches: + - 'master' + +jobs: + ensure-sha-pinned: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Ensure SHA pinned actions + uses: zgosalvez/github-actions-ensure-sha-pinned-actions@b88cd0aad2c36a63e42c71f81cb1958fed95ac87 # v3.0.10 + with: + # slsa-github-generator requires using a semver tag for reusable workflows. + # See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators + allowlist: | + slsa-framework/slsa-github-generator diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 41666f4..3b772df 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -7,12 +7,16 @@ on: - synchronize - reopened +permissions: {} + jobs: build: runs-on: ubuntu-latest - outputs: - profiles: ${{ steps.profiles.outputs.matrix }} steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit - name: Checkout uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - name: Setup Go diff --git a/.github/workflows/pr-label.yaml b/.github/workflows/pr-label.yaml index ad76c07..8a05831 100644 --- a/.github/workflows/pr-label.yaml +++ b/.github/workflows/pr-label.yaml @@ -3,12 +3,20 @@ name: pr-label on: pull_request: +permissions: {} + jobs: size-label: runs-on: ubuntu-latest if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} + permissions: + pull-requests: write steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit - name: size-label uses: "pascalgn/size-label-action@bbbaa0d5ccce8e2e76254560df5c64b82dac2e12" env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/pr-stale.yaml b/.github/workflows/pr-stale.yaml new file mode 100644 index 0000000..4e1a39c --- /dev/null +++ b/.github/workflows/pr-stale.yaml @@ -0,0 +1,18 @@ +name: pr-stale +on: + schedule: + - cron: '30 1 * * *' + +permissions: {} + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + with: + days-before-close: '120' + stale-pr-label: stale + repo-token: ${{ github.token }} diff --git a/.github/workflows/pr-trivy.yaml b/.github/workflows/pr-trivy.yaml new file mode 100644 index 0000000..cfa44a5 --- /dev/null +++ b/.github/workflows/pr-trivy.yaml @@ -0,0 +1,28 @@ +name: pr-trivy +on: pull_request + +permissions: {} + +jobs: + trivy: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + + - name: Trivy fs scan + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 + with: + scan-type: 'fs' + ignore-unfixed: true + scanners: license,vuln,secret + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/rebase.yaml b/.github/workflows/rebase.yaml index 2b248ad..1449e06 100644 --- a/.github/workflows/rebase.yaml +++ b/.github/workflows/rebase.yaml @@ -6,16 +6,25 @@ on: issue_comment: types: [created] +permissions: {} + jobs: rebase: if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') runs-on: ubuntu-latest + permissions: + contents: write # needed to force push steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + - name: Checkout the latest code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: fetch-depth: 0 - name: Automatic Rebase - uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 #1.8 + uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8 env: - GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 369635d..2e05dd4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,16 +4,25 @@ on: tags: - 'v*' +permissions: {} + permissions: contents: write # needed to write releases id-token: write # needed for keyless signing packages: write # needed for ghcr access jobs: - build: - name: Build + release: + permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit - name: Checkout code uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 with: @@ -32,10 +41,10 @@ jobs: - uses: anchore/sbom-action/download-syft@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5 # v0.17.0 - name: Create release and SBOM if: startsWith(github.ref, 'refs/tags/v') - uses: goreleaser/goreleaser-action@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5.1.0 + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 with: version: latest - args: release --rm-dist --skip-validate + args: release --clean --skip=validate env: RUNNER_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ secrets.DOODLE_OSS_BOT}} diff --git a/.github/workflows/report-on-vulnerabilities.yaml b/.github/workflows/report-on-vulnerabilities.yaml new file mode 100644 index 0000000..3a7c48e --- /dev/null +++ b/.github/workflows/report-on-vulnerabilities.yaml @@ -0,0 +1,79 @@ +name: report-on-vulnerabilities + +permissions: {} + +on: + workflow_dispatch: {} + schedule: + - cron: '0 6 * * *' + +jobs: + scan: + runs-on: ubuntu-latest + outputs: + results: ${{ steps.parse-results.outputs.results }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + + - name: Scan for vulnerabilities + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 + with: + image-ref: ghcr.io/doodlescheduling/flux-build:latest + format: json + scanners: license,vuln,secret + ignore-unfixed: false + severity: HIGH,CRITICAL + output: scan.json + + - name: Parse scan results + id: parse-results + continue-on-error: true + run: | + VULNS=$(cat scan.json | jq '.Results[] | length') + if [[ $VULNS -eq 0 ]] + then + echo "No vulnerabilities found, halting" + echo "results=nothing" >> $GITHUB_OUTPUT + else + echo "Vulnerabilities found, creating issue" + echo "results=found" >> $GITHUB_OUTPUT + fi + + - name: Upload vulnerability scan report + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + if: steps.parse-results.outputs.results == 'found' + with: + name: scan.json + path: scan.json + if-no-files-found: error + + open-issue: + runs-on: ubuntu-latest + if: needs.scan.outputs.results == 'found' + needs: scan + permissions: + issues: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Download scan + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: scan.json + - name: Set scan output + id: set-scan-output + run: echo "results=$(cat scan.json | jq -c)" >> $GITHUB_OUTPUT + - uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RESULTS: ${{ steps.set-scan-output.outputs.results }} + with: + filename: .github/ISSUE_TEMPLATE/VULN-TEMPLATE.md diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml index 6185349..33e5d46 100644 --- a/.github/workflows/scan.yaml +++ b/.github/workflows/scan.yaml @@ -4,32 +4,25 @@ on: push: branches: [ master ] pull_request: + branches: [ master ] schedule: - cron: '18 10 * * 3' -permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for codeQL to write security events +permissions: {} jobs: - fossa: - name: fossa - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 - - name: Run FOSSA scan and upload build data - uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0 - with: - # FOSSA Push-Only API Token - fossa-api-key: 956b9b92c5b16eeca1467cebe104f2c3 - github-token: ${{ github.token }} - codeql: - name: codeql runs-on: ubuntu-latest + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for codeQL to write security events steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Initialize CodeQL uses: github/codeql-action/init@f0a12816612c7306b485a22cb164feb43c6df818 # codeql-bundle-20221020 with: diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml new file mode 100644 index 0000000..8db30c4 --- /dev/null +++ b/.github/workflows/scorecard.yaml @@ -0,0 +1,45 @@ +name: scorecard +on: + branch_protection_rule: + schedule: + - cron: '18 14 * * 5' + push: + branches: [ "master" ] + +permissions: {} + +jobs: + scorecard: + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: "Upload artifact" + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index 7acec3f..6043c49 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![release](https://img.shields.io/github/release/DoodleScheduling/flux-build/all.svg)](https://github.com/DoodleScheduling/flux-build/releases) [![release](https://github.com/doodlescheduling/flux-build/actions/workflows/release.yaml/badge.svg)](https://github.com/doodlescheduling/flux-build/actions/workflows/release.yaml) [![report](https://goreportcard.com/badge/github.com/DoodleScheduling/flux-build)](https://goreportcard.com/report/github.com/DoodleScheduling/flux-build) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/DoodleScheduling/flux-build/badge)](https://api.securityscorecards.dev/projects/github.com/DoodleScheduling/flux-build) [![Coverage Status](https://coveralls.io/repos/github/DoodleScheduling/flux-build/badge.svg?branch=master)](https://coveralls.io/github/DoodleScheduling/flux-build?branch=master) [![license](https://img.shields.io/github/license/DoodleScheduling/flux-build.svg)](https://github.com/DoodleScheduling/flux-build/blob/master/LICENSE) diff --git a/internal/build/helm.go b/internal/build/helm.go index 77624b0..8f1f592 100644 --- a/internal/build/helm.go +++ b/internal/build/helm.go @@ -35,7 +35,6 @@ import ( "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" helmgetter "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/postrender" helmreg "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" corev1 "k8s.io/api/core/v1" @@ -272,11 +271,7 @@ func (h *Helm) renderRelease(ctx context.Context, hr helmv1.HelmRelease, values apiVersions = append(apiVersions, h.opts.APIVersions...) client.APIVersions = apiVersions - renderer, err := h.postRenderers(hr) - if err != nil { - return nil, err - } - client.PostRenderer = renderer + client.PostRenderer = postrenderer.BuildPostRenderers(&hr) // If user opted-in to install (or replace) CRDs, install them first. var legacyCRDsPolicy = helmv1.Create @@ -292,25 +287,6 @@ func (h *Helm) renderRelease(ctx context.Context, hr helmv1.HelmRelease, values return client.RunWithContext(ctx, chart, values) } -// Create post renderer instances from HelmRelease and combine them into -// a single combined post renderer. -func (h *Helm) postRenderers(hr helmv1.HelmRelease) (postrender.PostRenderer, error) { - var combinedRenderer = postrenderer.NewCombinedPostRenderer() - combinedRenderer.AddRenderer(postrenderer.NewPostRendererNamespace(&hr)) - - for _, r := range hr.Spec.PostRenderers { - if r.Kustomize != nil { - combinedRenderer.AddRenderer(postrenderer.NewPostRendererKustomize(r.Kustomize)) - } - } - combinedRenderer.AddRenderer(postrenderer.NewPostRendererOriginLabels(&hr)) - - if combinedRenderer.Len() == 0 { - return nil, nil - } - return &combinedRenderer, nil -} - func (h *Helm) validateCRDsPolicy(policy helmv1.CRDsPolicy, defaultValue helmv1.CRDsPolicy) (helmv1.CRDsPolicy, error) { switch policy { case "": diff --git a/internal/helm/postrenderer/build.go b/internal/helm/postrenderer/build.go new file mode 100644 index 0000000..7268732 --- /dev/null +++ b/internal/helm/postrenderer/build.go @@ -0,0 +1,59 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package postrenderer + +import ( + "encoding/json" + + "github.com/opencontainers/go-digest" + helmpostrender "helm.sh/helm/v3/pkg/postrender" + + helmv1 "github.com/fluxcd/helm-controller/api/v2beta1" +) + +// BuildPostRenderers creates the post-renderer instances from a HelmRelease +// and combines them into a single Combined post renderer. +func BuildPostRenderers(rel *helmv1.HelmRelease) helmpostrender.PostRenderer { + if rel == nil { + return nil + } + renderers := make([]helmpostrender.PostRenderer, 0) + renderers = append(renderers, NewPostRendererNamespace(rel)) + + for _, r := range rel.Spec.PostRenderers { + if r.Kustomize != nil { + renderers = append(renderers, &Kustomize{ + Patches: r.Kustomize.Patches, + Images: r.Kustomize.Images, + }) + } + } + renderers = append(renderers, NewOriginLabels(helmv1.GroupVersion.Group, rel.Namespace, rel.Name)) + if len(renderers) == 0 { + return nil + } + return NewCombined(renderers...) +} + +func Digest(algo digest.Algorithm, postrenders []helmv1.PostRenderer) digest.Digest { + digester := algo.Digester() + enc := json.NewEncoder(digester.Hash()) + if err := enc.Encode(postrenders); err != nil { + return "" + } + return digester.Digest() +} diff --git a/internal/helm/postrenderer/post_renderer.go b/internal/helm/postrenderer/post_renderer_combined.go similarity index 54% rename from internal/helm/postrenderer/post_renderer.go rename to internal/helm/postrenderer/post_renderer_combined.go index ff3c1b4..d42c9f7 100644 --- a/internal/helm/postrenderer/post_renderer.go +++ b/internal/helm/postrenderer/post_renderer_combined.go @@ -19,31 +19,25 @@ package postrenderer import ( "bytes" - "helm.sh/helm/v3/pkg/postrender" + helmpostrender "helm.sh/helm/v3/pkg/postrender" ) -// combinedPostRenderer, a collection of Helm PostRenders which are +// Combined is a collection of Helm PostRenders which are // invoked in the order of insertion. -type combinedPostRenderer struct { - renderers []postrender.PostRenderer +type Combined struct { + renderers []helmpostrender.PostRenderer } -func NewCombinedPostRenderer() combinedPostRenderer { - return combinedPostRenderer{ - renderers: make([]postrender.PostRenderer, 0), +func NewCombined(renderer ...helmpostrender.PostRenderer) *Combined { + pr := make([]helmpostrender.PostRenderer, 0) + pr = append(pr, renderer...) + return &Combined{ + renderers: pr, } } -func (c *combinedPostRenderer) AddRenderer(renderer postrender.PostRenderer) { - c.renderers = append(c.renderers, renderer) -} - -func (c *combinedPostRenderer) Len() int { - return len(c.renderers) -} - -func (c *combinedPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { - var result *bytes.Buffer = renderedManifests +func (c *Combined) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { + var result = renderedManifests for _, renderer := range c.renderers { result, err = renderer.Run(result) if err != nil { diff --git a/internal/helm/postrenderer/post_renderer_kustomize.go b/internal/helm/postrenderer/post_renderer_kustomize.go index ff8320a..172d2fd 100644 --- a/internal/helm/postrenderer/post_renderer_kustomize.go +++ b/internal/helm/postrenderer/post_renderer_kustomize.go @@ -21,24 +21,61 @@ import ( "encoding/json" "sync" - "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/resmap" kustypes "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filesys" "github.com/fluxcd/pkg/apis/kustomize" - - v2 "github.com/fluxcd/helm-controller/api/v2beta1" ) -type postRendererKustomize struct { - spec *v2.Kustomize +// Kustomize is a Helm post-render plugin that runs Kustomize. +type Kustomize struct { + // Patches is a list of patches to apply to the rendered manifests. + Patches []kustomize.Patch + // Images is a list of images to replace in the rendered manifests. + Images []kustomize.Image } -func NewPostRendererKustomize(spec *v2.Kustomize) *postRendererKustomize { - return &postRendererKustomize{ - spec: spec, +func (k *Kustomize) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { + fs := filesys.MakeFsInMemory() + cfg := kustypes.Kustomization{} + cfg.APIVersion = kustypes.KustomizationVersion + cfg.Kind = kustypes.KustomizationKind + cfg.Images = adaptImages(k.Images) + + // Add rendered Helm output as input resource to the Kustomization. + const input = "helm-output.yaml" + cfg.Resources = append(cfg.Resources, input) + if err := writeFile(fs, input, renderedManifests); err != nil { + return nil, err + } + + // Add patches. + for _, m := range k.Patches { + cfg.Patches = append(cfg.Patches, kustypes.Patch{ + Patch: m.Patch, + Target: adaptSelector(m.Target), + }) + } + + // Write kustomization config to file. + kustomization, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + if err := writeToFile(fs, "kustomization.yaml", kustomization); err != nil { + return nil, err } + resMap, err := buildKustomization(fs, ".") + if err != nil { + return nil, err + } + yaml, err := resMap.AsYaml() + if err != nil { + return nil, err + } + return bytes.NewBuffer(yaml), nil } func writeToFile(fs filesys.FileSystem, path string, content []byte) error { @@ -46,12 +83,10 @@ func writeToFile(fs filesys.FileSystem, path string, content []byte) error { if err != nil { return err } - _, err = helmOutput.Write(content) - if err != nil { + if _, err = helmOutput.Write(content); err != nil { return err } - - if err := helmOutput.Close(); err != nil { + if err = helmOutput.Close(); err != nil { return err } return nil @@ -62,12 +97,10 @@ func writeFile(fs filesys.FileSystem, path string, content *bytes.Buffer) error if err != nil { return err } - _, err = content.WriteTo(helmOutput) - if err != nil { + if _, err = content.WriteTo(helmOutput); err != nil { return err } - - if err := helmOutput.Close(); err != nil { + if err = helmOutput.Close(); err != nil { return err } return nil @@ -99,69 +132,10 @@ func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) { return } -func (k *postRendererKustomize) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { - fs := filesys.MakeFsInMemory() - cfg := kustypes.Kustomization{} - cfg.APIVersion = kustypes.KustomizationVersion - cfg.Kind = kustypes.KustomizationKind - cfg.Images = adaptImages(k.spec.Images) - - // Add rendered Helm output as input resource to the Kustomization. - const input = "helm-output.yaml" - cfg.Resources = append(cfg.Resources, input) - if err := writeFile(fs, input, renderedManifests); err != nil { - return nil, err - } - - // Add patches. - for _, m := range k.spec.Patches { - cfg.Patches = append(cfg.Patches, kustypes.Patch{ - Patch: m.Patch, - Target: adaptSelector(m.Target), - }) - } - - // Add strategic merge patches. - for _, m := range k.spec.PatchesStrategicMerge { - cfg.PatchesStrategicMerge = append(cfg.PatchesStrategicMerge, kustypes.PatchStrategicMerge(m.Raw)) - } - - // Add JSON 6902 patches. - for _, m := range k.spec.PatchesJSON6902 { - patch, err := json.Marshal(m.Patch) - if err != nil { - return nil, err - } - cfg.PatchesJson6902 = append(cfg.PatchesJson6902, kustypes.Patch{ - Patch: string(patch), - Target: adaptSelector(&m.Target), - }) - } - - // Write kustomization config to file. - kustomization, err := json.Marshal(cfg) - if err != nil { - return nil, err - } - if err := writeToFile(fs, "kustomization.yaml", kustomization); err != nil { - return nil, err - } - resMap, err := buildKustomization(fs, ".") - if err != nil { - return nil, err - } - yaml, err := resMap.AsYaml() - if err != nil { - return nil, err - } - return bytes.NewBuffer(yaml), nil -} - // TODO: remove mutex when kustomize fixes the concurrent map read/write panic var kustomizeRenderMutex sync.Mutex // buildKustomization wraps krusty.MakeKustomizer with the following settings: -// - reorder the resources just before output (Namespaces and Cluster roles/role bindings first, CRDs before CRs, Webhooks last) // - load files from outside the kustomization.yaml root // - disable plugins except for the builtin ones func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) { diff --git a/internal/helm/postrenderer/post_renderer_kustomize_test.go b/internal/helm/postrenderer/post_renderer_kustomize_test.go index 4085ff8..036c029 100644 --- a/internal/helm/postrenderer/post_renderer_kustomize_test.go +++ b/internal/helm/postrenderer/post_renderer_kustomize_test.go @@ -18,16 +18,14 @@ package postrenderer import ( "bytes" - "encoding/json" - "reflect" "testing" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + . "github.com/onsi/gomega" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/apis/kustomize" - v2 "github.com/fluxcd/helm-controller/api/v2beta1" + v2 "github.com/fluxcd/helm-controller/api/v2" ) const replaceImageMock = `apiVersion: v1 @@ -61,14 +59,12 @@ spec: func Test_postRendererKustomize_Run(t *testing.T) { tests := []struct { - name string - renderedManifests string - patches string - patchesStrategicMerge string - patchesJson6902 string - images string - expectManifests string - expectErr bool + name string + renderedManifests string + patches string + images string + expectManifests string + expectErr bool }{ { name: "image tag", @@ -121,12 +117,12 @@ spec: { name: "json 6902", renderedManifests: json6902Mock, - patchesJson6902: ` + patches: ` - target: version: v1 kind: Pod name: json6902 - patch: + patch: | - op: test path: /metadata/annotations/c value: foo @@ -188,33 +184,6 @@ metadata: d: "42" e: "42" name: json6902 -`, - }, - { - name: "strategic merge test", - renderedManifests: strategicMergeMock, - patchesStrategicMerge: ` -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx - spec: - template: - spec: - containers: - - name: nginx - image: nignx:latest -`, - expectManifests: `apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - template: - spec: - containers: - - image: nignx:latest - name: nginx `, }, { @@ -253,51 +222,39 @@ spec: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - spec, err := mockKustomize(tt.patches, tt.patchesStrategicMerge, tt.patchesJson6902, tt.images) - if err != nil { - t.Errorf("Run() mockKustomize returned %v", err) - return - } - k := &postRendererKustomize{ - spec: spec, + g := NewWithT(t) + + spec, err := mockKustomize(tt.patches, tt.images) + g.Expect(err).ToNot(HaveOccurred()) + + k := &Kustomize{ + Patches: spec.Patches, + Images: spec.Images, } gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests)) - if (err != nil) != tt.expectErr { - t.Errorf("Run() error = %v, expectErr %v", err, tt.expectErr) + if tt.expectErr { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gotModifiedManifests.String()).To(BeEmpty()) return } - if !reflect.DeepEqual(gotModifiedManifests, bytes.NewBufferString(tt.expectManifests)) { - t.Errorf("Run() gotModifiedManifests = %v, want %v", gotModifiedManifests, tt.expectManifests) - } + + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gotModifiedManifests).To(Equal(bytes.NewBufferString(tt.expectManifests))) }) } } -func mockKustomize(patches, patchesStrategicMerge, patchesJson6902, images string) (*v2.Kustomize, error) { +func mockKustomize(patches, images string) (*v2.Kustomize, error) { var targeted []kustomize.Patch if err := yaml.Unmarshal([]byte(patches), &targeted); err != nil { return nil, err } - b, err := yaml.YAMLToJSON([]byte(patchesStrategicMerge)) - if err != nil { - return nil, err - } - var strategicMerge []v1.JSON - if err := json.Unmarshal(b, &strategicMerge); err != nil { - return nil, err - } - var json6902 []kustomize.JSON6902Patch - if err := yaml.Unmarshal([]byte(patchesJson6902), &json6902); err != nil { - return nil, err - } var imgs []kustomize.Image if err := yaml.Unmarshal([]byte(images), &imgs); err != nil { return nil, err } return &v2.Kustomize{ - Patches: targeted, - PatchesStrategicMerge: strategicMerge, - PatchesJSON6902: json6902, - Images: imgs, + Patches: targeted, + Images: imgs, }, nil } diff --git a/internal/helm/postrenderer/post_renderer_namespace.go b/internal/helm/postrenderer/post_renderer_namespace.go index 2758bfc..aa5d845 100644 --- a/internal/helm/postrenderer/post_renderer_namespace.go +++ b/internal/helm/postrenderer/post_renderer_namespace.go @@ -4,12 +4,12 @@ import ( "bytes" "encoding/json" - v2 "github.com/fluxcd/helm-controller/api/v2beta1" - "sigs.k8s.io/kustomize/api/filesys" + helmv1 "github.com/fluxcd/helm-controller/api/v2beta1" kustypes "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filesys" ) -func NewPostRendererNamespace(release *v2.HelmRelease) *postRendererNamespace { +func NewPostRendererNamespace(release *helmv1.HelmRelease) *postRendererNamespace { ns := release.GetReleaseNamespace() if ns == "" { ns = "default" diff --git a/internal/helm/postrenderer/post_renderer_origin_labels.go b/internal/helm/postrenderer/post_renderer_origin_labels.go index 344f8a5..ce539e4 100644 --- a/internal/helm/postrenderer/post_renderer_origin_labels.go +++ b/internal/helm/postrenderer/post_renderer_origin_labels.go @@ -24,23 +24,23 @@ import ( "sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/resmap" kustypes "sigs.k8s.io/kustomize/api/types" - - v2 "github.com/fluxcd/helm-controller/api/v2beta1" ) -func NewPostRendererOriginLabels(release *v2.HelmRelease) *postRendererOriginLabels { - return &postRendererOriginLabels{ - name: release.ObjectMeta.Name, - namespace: release.ObjectMeta.Namespace, +func NewOriginLabels(group, namespace, name string) *OriginLabels { + return &OriginLabels{ + group: group, + name: name, + namespace: namespace, } } -type postRendererOriginLabels struct { +type OriginLabels struct { + group string name string namespace string } -func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { +func (k *OriginLabels) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { resFactory := provider.NewDefaultDepProvider().GetResourceFactory() resMapFactory := resmap.NewFactory(resFactory) @@ -50,7 +50,7 @@ func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifie } labelTransformer := builtins.LabelTransformerPlugin{ - Labels: originLabels(k.name, k.namespace), + Labels: originLabels(k.group, k.namespace, k.name), FieldSpecs: []kustypes.FieldSpec{ {Path: "metadata/labels", CreateIfNotPresent: true}, }, @@ -67,9 +67,9 @@ func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifie return bytes.NewBuffer(yaml), nil } -func originLabels(name, namespace string) map[string]string { +func originLabels(group, namespace, name string) map[string]string { return map[string]string{ - fmt.Sprintf("%s/name", v2.GroupVersion.Group): name, - fmt.Sprintf("%s/namespace", v2.GroupVersion.Group): namespace, + fmt.Sprintf("%s/name", group): name, + fmt.Sprintf("%s/namespace", group): namespace, } } diff --git a/internal/helm/postrenderer/post_renderer_origin_labels_test.go b/internal/helm/postrenderer/post_renderer_origin_labels_test.go index a7e64be..e73ec63 100644 --- a/internal/helm/postrenderer/post_renderer_origin_labels_test.go +++ b/internal/helm/postrenderer/post_renderer_origin_labels_test.go @@ -18,8 +18,9 @@ package postrenderer import ( "bytes" - "reflect" "testing" + + . "github.com/onsi/gomega" ) const mixedResourceMock = `apiVersion: v1 @@ -35,7 +36,7 @@ metadata: existing: label ` -func Test_postRendererOriginLabels_Run(t *testing.T) { +func Test_OriginLabels_Run(t *testing.T) { tests := []struct { name string renderedManifests string @@ -66,18 +67,17 @@ metadata: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - k := &postRendererOriginLabels{ - name: "name", - namespace: "namespace", - } + g := NewWithT(t) + + k := NewOriginLabels("helm.toolkit.fluxcd.io", "namespace", "name") gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests)) - if (err != nil) != tt.expectErr { - t.Errorf("Run() error = %v, expectErr %v", err, tt.expectErr) + if tt.expectErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(gotModifiedManifests.String()).To(BeEmpty()) return } - if !reflect.DeepEqual(gotModifiedManifests, bytes.NewBufferString(tt.expectManifests)) { - t.Errorf("Run() gotModifiedManifests = %v, want %v", gotModifiedManifests, tt.expectManifests) - } + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gotModifiedManifests).To(Equal(bytes.NewBufferString(tt.expectManifests))) }) } }