Repository: GoogleContainerTools/skaffold
Pull request #7396 (2022-05-11 21:30:19)

fix: update pkg/webhook to support previewing v2 docs site from github label
Initially I ported over the webhook to a  new cluster `docs-v2` but the webhook did not actually generate a preview site for the v2 docs and was still designed for v1.  This PR updates the webhook-v2 container and cluster to work for a new label `docs-modifications-v2` and generate a preview for v2 docs

container-tools-bot: Please visit [http://34.94.20.17:1313](http://34.94.20.17:1313) to view changes to the docs.
codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7396](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (73e382f) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `3.92%`.
> The diff coverage is `55.51%`.

```diff
@@            Coverage Diff             @@
##             main    #7396      +/-   ##
==========================================
- Coverage   70.48%   66.55%   -3.93%     
==========================================
  Files         515      565      +50     
  Lines       23150    27113    +3963     
==========================================
+ Hits        16317    18045    +1728     
- Misses       5776     7763    +1987     
- Partials     1057     1305     +248     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `50.00% <0.00%> (-3.85%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `36.66% <12.50%> (-4.72%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `54.09% <38.88%> (-22.38%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdmVyaWZ5Lmdv) | `41.17% <41.17%> (ø)` | |
| [cmd/skaffold/app/cmd/lint.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbGludC5nbw==) | `42.85% <42.85%> (ø)` | |
| ... and [286 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7396/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)

container-tools-bot: Error creating deployment docs-controller-deployment-7396, please visit https://storage.googleapis.com/webhook-logs/logs-7396-1652395213821332401 to view logs.
container-tools-bot: Please visit [http://34.123.82.70:1313](http://34.123.82.70:1313) to view changes to the docs.

Patch:
diff --git a/deploy/docs-v2/cloudbuild-release.yaml b/deploy/docs-v2/cloudbuild-release.yaml
index fe87a30c8..ff1ac86fc 100644
--- a/deploy/docs-v2/cloudbuild-release.yaml
+++ b/deploy/docs-v2/cloudbuild-release.yaml
@@ -6,22 +6,22 @@ steps:
   args:
   - '-c'
   - |
-    docker pull gcr.io/$PROJECT_ID/docs-controller:latest || exit 0
+    docker pull gcr.io/$PROJECT_ID/docs-controller-v2:latest || exit 0
   # until https://github.com/GoogleCloudPlatform/cloud-builders/issues/253 is fixed
 
-# Build and push latest version of the docs-controller image
+# Build and push latest version of the docs-controller-v2 image
 - name: gcr.io/cloud-builders/docker
   args:
   - 'build'
   - '-t'
-  - 'gcr.io/$PROJECT_ID/docs-controller:latest'
+  - 'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
   - '--cache-from'
-  - 'gcr.io/$PROJECT_ID/docs-controller:latest'
+  - 'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
   - '--target'
   - 'runtime_deps'
   - 'deploy/webhook-v2'
 
-- name: gcr.io/$PROJECT_ID/docs-controller:latest
+- name: gcr.io/$PROJECT_ID/docs-controller-v2:latest
   env:
     - 'HUGO_ENV=production'
   args:
@@ -31,5 +31,5 @@ steps:
     deploy/docs-v2/build.sh https://skaffold-v2.firebaseapp.com && cd docs-v2 && firebase deploy --only hosting:release --project $PROJECT_ID
 
 images: [
-  'gcr.io/$PROJECT_ID/docs-controller:latest'
+  'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
 ]
diff --git a/deploy/docs-v2/cloudbuild.yaml b/deploy/docs-v2/cloudbuild.yaml
index ac455390c..898f11456 100644
--- a/deploy/docs-v2/cloudbuild.yaml
+++ b/deploy/docs-v2/cloudbuild.yaml
@@ -1,27 +1,27 @@
 steps:
 
-# Download the latest version of the docs-controller image
+# Download the latest version of the docs-controller-v2 image
 - name: gcr.io/cloud-builders/docker
   entrypoint: 'bash'
   args:
   - '-c'
   - |
-    docker pull gcr.io/$PROJECT_ID/docs-controller:latest || exit 0
+    docker pull gcr.io/$PROJECT_ID/docs-controller-v2:latest || exit 0
   # until https://github.com/GoogleCloudPlatform/cloud-builders/issues/253 is fixed
 
-# Build and push latest version of the docs-controller image
+# Build and push latest version of the docs-controller-v2 image
 - name: gcr.io/cloud-builders/docker
   args:
   - 'build'
   - '-t'
-  - 'gcr.io/$PROJECT_ID/docs-controller:latest'
+  - 'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
   - '--cache-from'
-  - 'gcr.io/$PROJECT_ID/docs-controller:latest'
+  - 'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
   - '--target'
   - 'runtime_deps'
   - 'deploy/webhook-v2'
 
-- name: gcr.io/$PROJECT_ID/docs-controller:latest
+- name: gcr.io/$PROJECT_ID/docs-controller-v2:latest
   env:
     - 'HUGO_ENV=production'
   args:
@@ -31,4 +31,4 @@ steps:
     deploy/docs-v2/build.sh https://skaffold-v2-latest.firebaseapp.com && cd docs-v2 && firebase deploy --only hosting:head --project $PROJECT_ID
 
 images:
-- 'gcr.io/$PROJECT_ID/docs-controller:latest'
+- 'gcr.io/$PROJECT_ID/docs-controller-v2:latest'
diff --git a/deploy/webhook-v2/Dockerfile b/deploy/webhook-v2/Dockerfile
index f7aed4ed2..b7ab72550 100644
--- a/deploy/webhook-v2/Dockerfile
+++ b/deploy/webhook-v2/Dockerfile
@@ -49,7 +49,8 @@ COPY --from=download-kubectl /kubectl /usr/local/bin/
 FROM golang:1.17 as webhook
 WORKDIR /skaffold
 COPY . .
-RUN go build -o /webhook webhook/webhook.go
+# TODO(aaron-prindle) pass ldflags for -v2
+RUN go build -o /webhook -ldflags="-X 'github.com/GoogleContainerTools/skaffold/pkg/webhook/constants.DocsVersion=-v2'" webhook/webhook.go
 
 FROM runtime_deps
 COPY --from=webhook /webhook /webhook
diff --git a/deploy/webhook-v2/cloudbuild.yaml b/deploy/webhook-v2/cloudbuild.yaml
index f97124ad0..76965bb90 100644
--- a/deploy/webhook-v2/cloudbuild.yaml
+++ b/deploy/webhook-v2/cloudbuild.yaml
@@ -1,11 +1,11 @@
 steps:
-# Download the latest version of the docs-controller image
+# Download the latest version of the docs-controller-v2 image
 - name: gcr.io/cloud-builders/docker
   entrypoint: 'bash'
   args:
     - '-c'
     - |
-      docker pull gcr.io/$PROJECT_ID/docs-controller:latest || exit 0
+      docker pull gcr.io/$PROJECT_ID/docs-controller-v2:latest || exit 0
   # until https://github.com/GoogleCloudPlatform/cloud-builders/issues/253 is fixed
 
   # Get cluster credentials
diff --git a/deploy/webhook-v2/deployment.yaml b/deploy/webhook-v2/deployment.yaml
index b8971d893..4cf11b3c2 100644
--- a/deploy/webhook-v2/deployment.yaml
+++ b/deploy/webhook-v2/deployment.yaml
@@ -20,7 +20,7 @@ subjects:
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: docs-controller
+  name: docs-controller-v2
   labels:
     run: docs
 spec:
@@ -34,8 +34,8 @@ spec:
     spec:
       serviceAccountName: admin-serviceaccount
       containers:
-      - name: docs-controller
-        image: gcr.io/k8s-skaffold/docs-controller:latest
+      - name: docs-controller-v2
+        image: gcr.io/k8s-skaffold/docs-controller-v2:latest
         args: ["/webhook"]
         volumeMounts:
         - name: webhook
diff --git a/deploy/webhook-v2/skaffold.yaml b/deploy/webhook-v2/skaffold.yaml
index 74668567b..286970af0 100644
--- a/deploy/webhook-v2/skaffold.yaml
+++ b/deploy/webhook-v2/skaffold.yaml
@@ -2,11 +2,11 @@ apiVersion: skaffold/v1alpha5
 kind: Config
 build:
     artifacts:
-    - image: gcr.io/k8s-skaffold/docs-controller
+    - image: gcr.io/k8s-skaffold/docs-controller-v2
       docker:
         dockerfile: deploy/webhook-v2/Dockerfile
         cacheFrom:
-          - gcr.io/k8s-skaffold/docs-controller
+          - gcr.io/k8s-skaffold/docs-controller-v2
 deploy:
   kubectl:
     manifests:
diff --git a/pkg/skaffold/deploy/deploy_mux.go b/pkg/skaffold/deploy/deploy_mux.go
index 3b15ff02f..f94cc71dc 100644
--- a/pkg/skaffold/deploy/deploy_mux.go
+++ b/pkg/skaffold/deploy/deploy_mux.go
@@ -170,23 +170,5 @@ func (m DeployerMux) Cleanup(ctx context.Context, w io.Writer, dryRun bool, list
 	return nil
 }
 
-// TODO move this to RenderMux https://github.com/GoogleContainerTools/skaffold/issues/7330
-// func (m DeployerMux) Render(ctx context.Context, w io.Writer, as []graph.Artifact, offline bool, filepath string) error {
-//	resources, buf := []string{}, &bytes.Buffer{}
-//	for _, deployer := range m.deployers {
-//		ctx, endTrace := instrumentation.StartTrace(ctx, "Render")
-//		buf.Reset()
-//		if err := deployer.Render(ctx, buf, as, offline, "" /* never write to files */); err != nil {
-//			endTrace(instrumentation.TraceEndError(err))
-//			return err
-//		}
-//		resources = append(resources, buf.String())
-//		endTrace()
-//	}
-//
-//	allResources := strings.Join(resources, "\n---\n")
-//	return manifest.Write(strings.TrimSpace(allResources), filepath, w)
-//}
-
 // TrackBuildArtifacts should *only* be called on individual deployers. This is a noop.
 func (m DeployerMux) TrackBuildArtifacts(_ []graph.Artifact) {}
diff --git a/pkg/skaffold/deploy/kubectl/cli.go b/pkg/skaffold/deploy/kubectl/cli.go
index efaf16e4b..7ddc73902 100644
--- a/pkg/skaffold/deploy/kubectl/cli.go
+++ b/pkg/skaffold/deploy/kubectl/cli.go
@@ -61,7 +61,6 @@ type Config interface {
 	Tail() bool
 	PipelineForImage(imageName string) (latest.Pipeline, bool)
 	JSONParseConfig() latest.JSONParseConfig
-	GetRenderConfig() *latest.RenderConfig
 }
 
 func NewCLI(cfg Config, flags latest.KubectlFlags, defaultNamespace string) CLI {
diff --git a/pkg/skaffold/platform/resolver.go b/pkg/skaffold/platform/resolver.go
index b5ea0fa53..49c476f7f 100644
--- a/pkg/skaffold/platform/resolver.go
+++ b/pkg/skaffold/platform/resolver.go
@@ -84,7 +84,7 @@ func NewResolver(ctx context.Context, pipelines []latest.Pipeline, cliPlatformsS
 			} else if p := platforms.Intersect(fromClusterNodes); p.IsNotEmpty() {
 				platforms = p
 			} else {
-				return r, fmt.Errorf("build target platforms %q do not match active kubernetes cluster node platforms %q", platforms, fromClusterNodes)
+				log.Entry(ctx).Warnf("build target platforms %q do not match active kubernetes cluster node platforms %q", platforms, fromClusterNodes)
 			}
 		}
 		instrumentation.AddResolvedBuildTargetPlatforms(platforms.String())
diff --git a/pkg/skaffold/render/config.go b/pkg/skaffold/render/config.go
index d992cf9ab..53ee05e63 100644
--- a/pkg/skaffold/render/config.go
+++ b/pkg/skaffold/render/config.go
@@ -21,7 +21,6 @@ import (
 )
 
 type Config interface {
-	GetRenderConfig() *latest.RenderConfig
 	GetWorkingDir() string
 	TransformAllowList() []latest.ResourceFilter
 	TransformDenyList() []latest.ResourceFilter
diff --git a/pkg/skaffold/render/renderer/kpt/kpt.go b/pkg/skaffold/render/renderer/kpt/kpt.go
index 82b7ea52a..e8ff2f3ca 100644
--- a/pkg/skaffold/render/renderer/kpt/kpt.go
+++ b/pkg/skaffold/render/renderer/kpt/kpt.go
@@ -59,17 +59,16 @@ const (
 	DryFileName = "manifests.yaml"
 )
 
-func New(cfg render.Config, hydrationDir string,
-	labels map[string]string) (*Kpt, error) {
-	generator := generate.NewGenerator(cfg.GetWorkingDir(), cfg.GetRenderConfig().Generate)
+func New(cfg render.Config, rCfg latest.RenderConfig, hydrationDir string, labels map[string]string) (*Kpt, error) {
+	generator := generate.NewGenerator(cfg.GetWorkingDir(), rCfg.Generate)
 	transformAllowlist, transformDenylist, err := rUtil.ConsolidateTransformConfiguration(cfg)
 	if err != nil {
 		return nil, err
 	}
 	var validator validate.Validator
-	if cfg.GetRenderConfig().Validate != nil {
+	if rCfg.Validate != nil {
 		var err error
-		validator, err = validate.NewValidator(*cfg.GetRenderConfig().Validate)
+		validator, err = validate.NewValidator(*rCfg.Validate)
 		if err != nil {
 			return nil, err
 		}
@@ -78,9 +77,9 @@ func New(cfg render.Config, hydrationDir string,
 	}
 
 	var transformer transform.Transformer
-	if cfg.GetRenderConfig().Transform != nil {
+	if rCfg.Transform != nil {
 		var err error
-		transformer, err = transform.NewTransformer(*cfg.GetRenderConfig().Transform)
+		transformer, err = transform.NewTransformer(*rCfg.Transform)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/skaffold/render/renderer/kubectl/kubectl.go b/pkg/skaffold/render/renderer/kubectl/kubectl.go
index c867655a4..7ce5637cb 100644
--- a/pkg/skaffold/render/renderer/kubectl/kubectl.go
+++ b/pkg/skaffold/render/renderer/kubectl/kubectl.go
@@ -40,8 +40,8 @@ type Kubectl struct {
 	transformDenylist  map[apimachinery.GroupKind]latest.ResourceFilter
 }
 
-func New(cfg render.Config, labels map[string]string) (Kubectl, error) {
-	generator := generate.NewGenerator(cfg.GetWorkingDir(), cfg.GetRenderConfig().Generate)
+func New(cfg render.Config, rCfg latest.RenderConfig, labels map[string]string) (Kubectl, error) {
+	generator := generate.NewGenerator(cfg.GetWorkingDir(), rCfg.Generate)
 	transformAllowlist, transformDenylist, err := util.ConsolidateTransformConfiguration(cfg)
 	if err != nil {
 		return Kubectl{}, err
diff --git a/pkg/skaffold/render/renderer/noop/noop.go b/pkg/skaffold/render/renderer/noop/noop.go
index cdf575f60..ac975e434 100644
--- a/pkg/skaffold/render/renderer/noop/noop.go
+++ b/pkg/skaffold/render/renderer/noop/noop.go
@@ -30,7 +30,7 @@ import (
 // TODO(marlongamez): add Helm renderer which uses helm template and can feed into deployers
 type Noop struct{}
 
-func New(_ *latest.RenderConfig, _, _ string, _ map[string]string) (Noop, error) {
+func New(_ latest.RenderConfig, _, _ string, _ map[string]string) (Noop, error) {
 	return Noop{}, nil
 }
 
diff --git a/pkg/skaffold/render/renderer/render_mux.go b/pkg/skaffold/render/renderer/render_mux.go
new file mode 100644
index 000000000..05f45826e
--- /dev/null
+++ b/pkg/skaffold/render/renderer/render_mux.go
@@ -0,0 +1,73 @@
+/*
+Copyright 2022 The Skaffold 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 renderer
+
+import (
+	"context"
+	"io"
+	"strconv"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
+	eventV2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event/v2"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringset"
+)
+
+// RenderMux forwards all method calls to the renderers it contains.
+// When encountering an error, it aborts and returns the error. Otherwise,
+// it collects the results and returns all the manifests.
+type RenderMux struct {
+	renderers []Renderer
+}
+
+func NewRenderMux(renderers []Renderer) Renderer {
+	return RenderMux{renderers: renderers}
+}
+
+func (r RenderMux) Render(ctx context.Context, out io.Writer, artifacts []graph.Artifact, offline bool, outputPath string) (manifest.ManifestList, error) {
+	allManifests := manifest.ManifestList{}
+	for i, renderer := range r.renderers {
+		eventV2.RendererInProgress(i)
+		w, ctx := output.WithEventContext(ctx, out, constants.Render, strconv.Itoa(i))
+		ctx, endTrace := instrumentation.StartTrace(ctx, "Render")
+		ms, err := renderer.Render(ctx, w, artifacts, offline, "" /* never write to files */)
+		if err != nil {
+			eventV2.RendererFailed(i, err)
+			endTrace(instrumentation.TraceEndError(err))
+			return nil, err
+		}
+		allManifests = append(allManifests, ms...)
+		eventV2.RendererSucceeded(i)
+		endTrace()
+	}
+	return allManifests, nil
+}
+
+func (r RenderMux) ManifestDeps() ([]string, error) {
+	deps := stringset.New()
+	for _, renderer := range r.renderers {
+		result, err := renderer.ManifestDeps()
+		if err != nil {
+			return nil, err
+		}
+		deps.Insert(result...)
+	}
+	return deps.ToList(), nil
+}
diff --git a/pkg/skaffold/render/renderer/renderer.go b/pkg/skaffold/render/renderer/renderer.go
index ca4533fea..c59ea4a97 100644
--- a/pkg/skaffold/render/renderer/renderer.go
+++ b/pkg/skaffold/render/renderer/renderer.go
@@ -27,6 +27,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/kpt"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/kubectl"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/noop"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 )
 
 type Renderer interface {
@@ -37,15 +38,14 @@ type Renderer interface {
 }
 
 // New creates a new Renderer object from the latestV2 API schema.
-func New(cfg render.Config, hydrationDir string, labels map[string]string, usingLegacyHelmDeploy bool) (Renderer, error) {
-	renderCfg := cfg.GetRenderConfig()
+func New(cfg render.Config, renderCfg latest.RenderConfig, hydrationDir string, labels map[string]string, usingLegacyHelmDeploy bool) (Renderer, error) {
 	if usingLegacyHelmDeploy {
 		return noop.New(renderCfg, cfg.GetWorkingDir(), hydrationDir, labels)
 	}
 	if renderCfg.Validate == nil && renderCfg.Transform == nil && renderCfg.Kpt == nil {
 		log.Entry(context.TODO()).Debug("setting up kubectl renderer")
-		return kubectl.New(cfg, labels)
+		return kubectl.New(cfg, renderCfg, labels)
 	}
 	log.Entry(context.TODO()).Infof("setting up kpt renderer")
-	return kpt.New(cfg, hydrationDir, labels)
+	return kpt.New(cfg, renderCfg, hydrationDir, labels)
 }
diff --git a/pkg/skaffold/runner/renderer.go b/pkg/skaffold/runner/renderer.go
new file mode 100644
index 000000000..6a3771d84
--- /dev/null
+++ b/pkg/skaffold/runner/renderer.go
@@ -0,0 +1,39 @@
+/*
+Copyright 2022 The Skaffold 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 runner
+
+import (
+	"context"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer"
+	runcontext "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext/v2"
+)
+
+// GetRenderer creates a renderer from a given RunContext and pipeline definitions.
+func GetRenderer(ctx context.Context, runCtx *runcontext.RunContext, hydrationDir string, labels map[string]string, usingLegacyHelmDeploy bool) (renderer.Renderer, error) {
+	rs := runCtx.Renderers()
+
+	var renderers []renderer.Renderer
+	for _, r := range rs {
+		r, err := renderer.New(runCtx, r, hydrationDir, labels, usingLegacyHelmDeploy)
+		if err != nil {
+			return nil, err
+		}
+		renderers = append(renderers, r)
+	}
+	return renderer.NewRenderMux(renderers), nil
+}
diff --git a/pkg/skaffold/runner/runcontext/v2/context.go b/pkg/skaffold/runner/runcontext/v2/context.go
index f900b0df1..58921bb44 100644
--- a/pkg/skaffold/runner/runcontext/v2/context.go
+++ b/pkg/skaffold/runner/runcontext/v2/context.go
@@ -85,6 +85,7 @@ func (ps Pipelines) Artifacts() []*latest.Artifact {
 	return artifacts
 }
 
+// TODO: Clean up code duplication
 func (ps Pipelines) DeployConfigs() []latest.DeployConfig {
 	var cfgs []latest.DeployConfig
 	for _, p := range ps.pipelines {
@@ -101,6 +102,14 @@ func (ps Pipelines) Deployers() []latest.DeployConfig {
 	return deployers
 }
 
+func (ps Pipelines) Renderers() []latest.RenderConfig {
+	var rcs []latest.RenderConfig
+	for _, p := range ps.pipelines {
+		rcs = append(rcs, p.Render)
+	}
+	return rcs
+}
+
 func (ps Pipelines) TestCases() []*latest.TestCase {
 	var tests []*latest.TestCase
 	for _, p := range ps.pipelines {
@@ -165,6 +174,8 @@ func (rc *RunContext) DeployConfigs() []latest.DeployConfig { return rc.Pipeline
 
 func (rc *RunContext) Deployers() []latest.DeployConfig { return rc.Pipelines.Deployers() }
 
+func (rc *RunContext) Renderers() []latest.RenderConfig { return rc.Pipelines.Renderers() }
+
 func (rc *RunContext) TestCases() []*latest.TestCase { return rc.Pipelines.TestCases() }
 
 func (rc *RunContext) StatusCheckDeadlineSeconds() int {
diff --git a/pkg/skaffold/runner/v2/new.go b/pkg/skaffold/runner/v2/new.go
index 951dc5746..681d044bc 100644
--- a/pkg/skaffold/runner/v2/new.go
+++ b/pkg/skaffold/runner/v2/new.go
@@ -32,7 +32,6 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
 	runcontext "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext/v2"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
@@ -82,7 +81,7 @@ func NewForConfig(ctx context.Context, runCtx *runcontext.RunContext) (*Skaffold
 		return nil, fmt.Errorf("getting render output path: %w", err)
 	}
 
-	renderer, err := renderer.New(runCtx, hydrationDir, labeller.Labels(), runCtx.UsingLegacyHelmDeploy())
+	renderer, err := runner.GetRenderer(ctx, runCtx, hydrationDir, labeller.Labels(), runCtx.UsingLegacyHelmDeploy())
 	if err != nil {
 		endTrace(instrumentation.TraceEndError(err))
 		return nil, fmt.Errorf("creating renderer: %w", err)
diff --git a/pkg/webhook/constants/constants.go b/pkg/webhook/constants/constants.go
index bdf953498..3c069687d 100644
--- a/pkg/webhook/constants/constants.go
+++ b/pkg/webhook/constants/constants.go
@@ -16,6 +16,15 @@ limitations under the License.
 
 package constants
 
+import "fmt"
+
+// DocsVersion defines the version of the docs currently ""(for v1) and "-v2"(for v2)
+// This variable is injected at build time based on what version of the controller is being built
+var DocsVersion string
+
+// DocsLabel kicks off the controller when added to a PR
+var DocsLabel = fmt.Sprintf("docs-modifications%s", DocsVersion)
+
 const (
 	// GithubAccessToken is the env variable auth for container-tools-bot is stored in
 	GithubAccessToken = "GITHUB_ACCESS_TOKEN"
@@ -35,9 +44,6 @@ const (
 	// when a PR is labeled
 	LabeledAction = "labeled"
 
-	// DocsLabel kicks off the controller when added to a PR
-	DocsLabel = "docs-modifications"
-
 	// Namespace is the namespace deployments and services will be created in
 	Namespace = "default"
 
diff --git a/pkg/webhook/kubernetes/deployment.go b/pkg/webhook/kubernetes/deployment.go
index 599fe1417..add4cab3d 100644
--- a/pkg/webhook/kubernetes/deployment.go
+++ b/pkg/webhook/kubernetes/deployment.go
@@ -93,7 +93,7 @@ func CreateDeployment(pr *github.PullRequestEvent, svc *v1.Service, externalIP s
 						{
 							Name:       "server",
 							Image:      constants.DeploymentImage,
-							Args:       []string{"deploy/docs/preview.sh", BaseURL(externalIP)},
+							Args:       []string{fmt.Sprintf("deploy/docs%s/preview.sh", constants.DocsVersion), BaseURL(externalIP)},
 							WorkingDir: repoPath,
 							VolumeMounts: []v1.VolumeMount{
 								{
diff --git a/webhook/webhook.go b/webhook/webhook.go
index 4d5a553f0..7a2917a0d 100644
--- a/webhook/webhook.go
+++ b/webhook/webhook.go
@@ -42,7 +42,7 @@ func main() {
 	http.HandleFunc("/receive", handleGithubEvent)
 	flag.Parse()
 	// Start the server
-	log.Println("Listening...")
+	log.Printf("Listening%s...\n", constants.DocsVersion)
 	log.Fatal(http.ListenAndServe(port, nil))
 }
 


Test patch:
diff --git a/integration/modules_test.go b/integration/modules_test.go
index cdc3cb5da..760650c56 100644
--- a/integration/modules_test.go
+++ b/integration/modules_test.go
@@ -26,9 +26,6 @@ import (
 )
 
 func TestModules_BuildDependency(t *testing.T) {
-	// TODO: Fix https://github.com/GoogleContainerTools/skaffold/issues/7035.
-	t.Skipf("Fix https://github.com/GoogleContainerTools/skaffold/issues/7035")
-
 	MarkIntegrationTest(t, CanRunWithoutGcp)
 	t.Run("build dependency between artifacts of different modules", func(t *testing.T) {
 		ns, client := SetupNamespace(t)
diff --git a/integration/render_test.go b/integration/render_test.go
index b5640115e..3c045ad96 100644
--- a/integration/render_test.go
+++ b/integration/render_test.go
@@ -73,14 +73,12 @@ spec:
 		tmpDir := t.NewTempDir()
 		tmpDir.Write("deployment.yaml", test.input).Chdir()
 
-		mockCfg := mockConfig{
-			renderConfig: &latest.RenderConfig{
-				Generate: latest.Generate{
-					RawK8s: []string{"deployment.yaml"}},
-			},
-			workingDir: tmpDir.Root(),
+		rc := latest.RenderConfig{
+			Generate: latest.Generate{
+				RawK8s: []string{"deployment.yaml"}},
 		}
-		r, err := kubectl.New(mockCfg, map[string]string{})
+		mockCfg := mockConfig{workingDir: tmpDir.Root()}
+		r, err := kubectl.New(mockCfg, rc, map[string]string{})
 		t.RequireNoError(err)
 		var b bytes.Buffer
 		l, err := r.Render(context.Background(), &b, test.builds, false, test.renderPath)
@@ -214,15 +212,12 @@ spec:
 			tmpDir := t.NewTempDir()
 			tmpDir.Write("deployment.yaml", test.input).
 				Chdir()
-
-			mockCfg := mockConfig{
-				renderConfig: &latest.RenderConfig{
-					Generate: latest.Generate{
-						RawK8s: []string{"deployment.yaml"}},
-				},
-				workingDir: tmpDir.Root(),
+			rc := latest.RenderConfig{
+				Generate: latest.Generate{
+					RawK8s: []string{"deployment.yaml"}},
 			}
-			r, err := kubectl.New(mockCfg, map[string]string{})
+			mockCfg := mockConfig{workingDir: tmpDir.Root()}
+			r, err := kubectl.New(mockCfg, rc, map[string]string{})
 			t.RequireNoError(err)
 			var b bytes.Buffer
 			l, err := r.Render(context.Background(), &b, test.builds, false, "")
@@ -753,11 +748,9 @@ spec:
 }
 
 type mockConfig struct {
-	renderConfig *latest.RenderConfig
-	workingDir   string
+	workingDir string
 }
 
-func (mc mockConfig) GetRenderConfig() *latest.RenderConfig       { return mc.renderConfig }
 func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
 func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
 func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
diff --git a/integration/testdata/modules/app1/skaffold.yaml b/integration/testdata/modules/app1/skaffold.yaml
index 38f1fed9e..b397b2a4b 100644
--- a/integration/testdata/modules/app1/skaffold.yaml
+++ b/integration/testdata/modules/app1/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v2beta25
+apiVersion: skaffold/v3alpha1
 kind: Config
 requires:
   - path: ../app2
@@ -16,7 +16,6 @@ build:
     requires:
     - image: image2
 
-deploy:
-  kubectl:
-    manifests:
-    - 'k8s.yaml'
+manifests:
+  rawYaml:
+  - 'k8s.yaml'
diff --git a/integration/testdata/modules/app2/skaffold.yaml b/integration/testdata/modules/app2/skaffold.yaml
index 82bf4eb30..659f45bf7 100644
--- a/integration/testdata/modules/app2/skaffold.yaml
+++ b/integration/testdata/modules/app2/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v2beta25
+apiVersion: skaffold/v3alpha1
 kind: Config
 requires:
   - path: ../app3
@@ -17,7 +17,6 @@ build:
     - image: image3
       alias: IMAGE3
 
-deploy:
-  kubectl:
-    manifests:
-    - 'k8s.yaml'
+manifests:
+  rawYaml:
+  - 'k8s.yaml'
diff --git a/integration/testdata/modules/app3/skaffold.yaml b/integration/testdata/modules/app3/skaffold.yaml
index adbd0887a..240f278b0 100644
--- a/integration/testdata/modules/app3/skaffold.yaml
+++ b/integration/testdata/modules/app3/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v2beta25
+apiVersion: skaffold/v3alpha1
 kind: Config
 build:
   tagPolicy:
@@ -12,7 +12,6 @@ build:
         SLEEP: "2"
         FAIL: "0"
 
-deploy:
-  kubectl:
-    manifests:
-    - 'k8s.yaml'
+manifests:
+  rawYaml:
+  - 'k8s.yaml'
diff --git a/integration/testdata/modules/skaffold.yaml b/integration/testdata/modules/skaffold.yaml
index 4860f1533..c014ec69d 100644
--- a/integration/testdata/modules/skaffold.yaml
+++ b/integration/testdata/modules/skaffold.yaml
@@ -1,6 +1,6 @@
-apiVersion: skaffold/v3beta2
+apiVersion: skaffold/v3alpha1
 kind: Config
 requires:
   - path: app1
   - path: app2
-  - path: app3
+  - path: app3
\ No newline at end of file
diff --git a/pkg/skaffold/deploy/deploy_mux_test.go b/pkg/skaffold/deploy/deploy_mux_test.go
index f3e94d3f8..ce8de0d1a 100644
--- a/pkg/skaffold/deploy/deploy_mux_test.go
+++ b/pkg/skaffold/deploy/deploy_mux_test.go
@@ -228,69 +228,3 @@ func TestDeployerMux_Dependencies(t *testing.T) {
 		})
 	}
 }
-
-// TODO: Add RendererMux https://github.com/GoogleContainerTools/skaffold/issues/7330
-// func TestDeployerMux_Render(t *testing.T) {
-//	tests := []struct {
-//		name           string
-//		render1        string
-//		render2        string
-//		err1           error
-//		err2           error
-//		expectedRender string
-//		shouldErr      bool
-//	}{
-//		{
-//			name:           "concatenates render results with separator",
-//			render1:        "manifest-1",
-//			render2:        "manifest-2",
-//			expectedRender: "manifest-1\n---\nmanifest-2\n",
-//		},
-//		{
-//			name:      "short-circuits when first call fails",
-//			render1:   "manifest-1",
-//			err1:      fmt.Errorf("failed in first"),
-//			render2:   "manifest-2",
-//			shouldErr: true,
-//		},
-//		{
-//			name:      "short-circuits when second call fails",
-//			render1:   "manifest-1",
-//			render2:   "manifest-2",
-//			err2:      fmt.Errorf("failed in first"),
-//			shouldErr: true,
-//		},
-//	}
-//
-//	for _, test := range tests {
-//		t.Run("output to writer "+test.name, func(t *testing.T) {
-//			deployerMux := NewDeployerMux([]Deployer{
-//				NewMockDeployer().WithRenderResult(test.render1).WithRenderErr(test.err1),
-//				NewMockDeployer().WithRenderResult(test.render2).WithRenderErr(test.err2),
-//			}, false)
-//
-//			buf := &bytes.Buffer{}
-//			err := deployerMux.Render(context.Background(), buf, nil, true, "")
-//			testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expectedRender, buf.String())
-//		})
-//	}
-//
-//	t.Run("output to file", func(t *testing.T) {
-//		// only check the good case here
-//		test := tests[0]
-//
-//		tmpDir := testutil.NewTempDir(t)
-//
-//		deployerMux := NewDeployerMux([]Deployer{
-//			NewMockDeployer().WithRenderResult(test.render1).WithRenderErr(test.err1),
-//			NewMockDeployer().WithRenderResult(test.render2).WithRenderErr(test.err2),
-//		}, false)
-//
-//		err := deployerMux.Render(context.Background(), nil, nil, true, tmpDir.Path("render"))
-//		testutil.CheckError(t, false, err)
-//
-//		file, _ := os.Open(tmpDir.Path("render"))
-//		content, _ := ioutil.ReadAll(file)
-//		testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expectedRender, string(content))
-//	})
-//}
diff --git a/pkg/skaffold/deploy/kubectl/kubectl_test.go b/pkg/skaffold/deploy/kubectl/kubectl_test.go
index 7b2638668..2df04c4e4 100644
--- a/pkg/skaffold/deploy/kubectl/kubectl_test.go
+++ b/pkg/skaffold/deploy/kubectl/kubectl_test.go
@@ -238,14 +238,15 @@ func TestKubectlV1RenderDeploy(t *testing.T) {
 			}, &label.DefaultLabeller{}, &test.kubectl, filepath.Join(tmpDir.Root(), constants.DefaultHydrationDir))
 			t.RequireNoError(err)
 
+			rc := latest.RenderConfig{Generate: test.generate}
 			mockCfg := &kubectlConfig{
 				RunContext: runcontext.RunContext{
 					WorkingDir: tmpDir.Root(),
 					Pipelines: runcontext.NewPipelines([]latest.Pipeline{
-						{Render: latest.RenderConfig{Generate: test.generate}}}),
+						{Render: rc}}),
 				},
 			}
-			r, err := kubectlR.New(mockCfg, map[string]string{})
+			r, err := kubectlR.New(mockCfg, rc, map[string]string{})
 			t.CheckNoError(err)
 			var b bytes.Buffer
 			m, errR := r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}},
@@ -328,15 +329,15 @@ func TestKubectlCleanup(t *testing.T) {
 				RunContext: runcontext.RunContext{Opts: config.SkaffoldOptions{Namespace: TestNamespace}},
 			}, &label.DefaultLabeller{}, &test.kubectl, filepath.Join(tmpDir.Root(), constants.DefaultHydrationDir))
 			t.RequireNoError(err)
-
+			rc := latest.RenderConfig{Generate: test.generate}
 			mockCfg := &kubectlConfig{
 				RunContext: runcontext.RunContext{
 					WorkingDir: tmpDir.Root(),
 					Pipelines: runcontext.NewPipelines([]latest.Pipeline{
-						{Render: latest.RenderConfig{Generate: test.generate}}}),
+						{Render: rc}}),
 				},
 			}
-			r, err := kubectlR.New(mockCfg, map[string]string{})
+			r, err := kubectlR.New(mockCfg, rc, map[string]string{})
 			t.CheckNoError(err)
 			var b bytes.Buffer
 			m, errR := r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}},
@@ -626,13 +627,14 @@ func TestGCSManifests(t *testing.T) {
 			if err := ioutil.WriteFile(manifest.ManifestTmpDir+"/deployment.yaml", []byte(DeploymentWebYAML), os.ModePerm); err != nil {
 				t.Fatal(err)
 			}
+			rc := latest.RenderConfig{Generate: test.generate}
 			mockCfg := &kubectlConfig{
 				RunContext: runcontext.RunContext{
 					Pipelines: runcontext.NewPipelines([]latest.Pipeline{
 						{Render: latest.RenderConfig{Generate: test.generate}}}),
 				},
 			}
-			r, err := kubectlR.New(mockCfg, map[string]string{})
+			r, err := kubectlR.New(mockCfg, rc, map[string]string{})
 			t.CheckNoError(err)
 			var b bytes.Buffer
 			m, errR := r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}},
diff --git a/pkg/skaffold/platform/resolver_test.go b/pkg/skaffold/platform/resolver_test.go
index 428ca4a30..7bb06e527 100644
--- a/pkg/skaffold/platform/resolver_test.go
+++ b/pkg/skaffold/platform/resolver_test.go
@@ -67,8 +67,11 @@ func TestResolver(t *testing.T) {
 			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
 				Platforms: []string{"windows/amd64"},
 				Artifacts: []*latest.Artifact{{ImageName: "img1"}, {ImageName: "img2"}}}}},
-			runMode:   config.RunModes.Dev,
-			shouldErr: true,
+			runMode: config.RunModes.Dev,
+			expected: map[string]Matcher{
+				"img1": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "386"}}},
+				"img2": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "386"}}},
+			},
 		},
 		{
 			description:      "cluster platform selected for `dev` mode",
diff --git a/pkg/skaffold/render/renderer/kpt/kpt_test.go b/pkg/skaffold/render/renderer/kpt/kpt_test.go
index 268c44c60..4a17bb354 100644
--- a/pkg/skaffold/render/renderer/kpt/kpt_test.go
+++ b/pkg/skaffold/render/renderer/kpt/kpt_test.go
@@ -62,14 +62,14 @@ metadata:
 func TestRender(t *testing.T) {
 	tests := []struct {
 		description     string
-		renderConfig    *latest.RenderConfig
+		renderConfig    latest.RenderConfig
 		config          *runcontext.RunContext
 		originalKptfile string
 		updatedKptfile  string
 	}{
 		{
 			description: "single manifest, no hydration rule",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 			},
 			originalKptfile: initKptfile,
@@ -82,7 +82,7 @@ pipeline: {}
 		},
 		{
 			description: "manifests with validation rule.",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 				Validate: &[]latest.Validator{{Name: "kubeval"}},
 			},
@@ -98,7 +98,7 @@ pipeline:
 		},
 		{
 			description: "manifests with updated validation rule.",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 				Validate: &[]latest.Validator{{Name: "kubeval"}},
 			},
@@ -121,7 +121,7 @@ pipeline:
 		},
 		{
 			description: "manifests with transformation rule.",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate:  latest.Generate{RawK8s: []string{"pod.yaml"}},
 				Transform: &[]latest.Transformer{{Name: "set-labels", ConfigMap: []string{"owner:tester"}}},
 			},
@@ -139,7 +139,7 @@ pipeline:
 		},
 		{
 			description: "manifests with updated transformation rule.",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate:  latest.Generate{RawK8s: []string{"pod.yaml"}},
 				Transform: &[]latest.Transformer{{Name: "set-labels", ConfigMap: []string{"owner:tester"}}},
 			},
@@ -171,10 +171,9 @@ pipeline:
 				Touch("empty.ignored").
 				Chdir()
 			mockCfg := mockConfig{
-				renderConfig: test.renderConfig,
-				workingDir:   tmpDirObj.Root(),
+				workingDir: tmpDirObj.Root(),
 			}
-			r, err := New(mockCfg, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
+			r, err := New(mockCfg, test.renderConfig, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
 			t.CheckNoError(err)
 			t.Override(&util.DefaultExecCommand,
 				testutil.CmdRun(fmt.Sprintf("kpt fn render %v",
@@ -236,14 +235,14 @@ inventory:
 			tmpDirObj.Write("pod.yaml", podYaml).
 				Write(filepath.Join(constants.DefaultHydrationDir, kptfile.KptFileName), test.originalKptfile).
 				Chdir()
+			renderConfig := latest.RenderConfig{
+				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
+				Validate: &[]latest.Validator{{Name: "kubeval"}},
+			}
 			mockCfg := mockConfig{
-				renderConfig: &latest.RenderConfig{
-					Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
-					Validate: &[]latest.Validator{{Name: "kubeval"}},
-				},
 				workingDir: tmpDirObj.Root(),
 			}
-			r, err := New(mockCfg, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
+			r, err := New(mockCfg, renderConfig, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
 			t.CheckNoError(err)
 			t.Override(&util.DefaultExecCommand,
 				testutil.CmdRun(fmt.Sprintf("kpt fn render %v",
@@ -259,11 +258,9 @@ inventory:
 }
 
 type mockConfig struct {
-	renderConfig *latest.RenderConfig
-	workingDir   string
+	workingDir string
 }
 
-func (mc mockConfig) GetRenderConfig() *latest.RenderConfig       { return mc.renderConfig }
 func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
 func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
 func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
diff --git a/pkg/skaffold/render/renderer/kubectl/kubectl_test.go b/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
index 033c45e7a..35b850a94 100644
--- a/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
+++ b/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
@@ -60,20 +60,20 @@ spec:
 func TestRender(t *testing.T) {
 	tests := []struct {
 		description  string
-		renderConfig *latest.RenderConfig
+		renderConfig latest.RenderConfig
 		labels       map[string]string
 		expected     string
 	}{
 		{
 			description: "single manifest with no labels",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 			},
 			expected: taggedPodYaml,
 		},
 		{
 			description: "single manifest with labels",
-			renderConfig: &latest.RenderConfig{
+			renderConfig: latest.RenderConfig{
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 			},
 			labels:   map[string]string{"run.id": "test"},
@@ -86,11 +86,8 @@ func TestRender(t *testing.T) {
 			tmpDirObj.Write("pod.yaml", podYaml).
 				Touch("empty.ignored").
 				Chdir()
-			mockCfg := mockConfig{
-				renderConfig: test.renderConfig,
-				workingDir:   tmpDirObj.Root(),
-			}
-			r, err := New(mockCfg, test.labels)
+			mockCfg := mockConfig{workingDir: tmpDirObj.Root()}
+			r, err := New(mockCfg, test.renderConfig, test.labels)
 			t.CheckNoError(err)
 			var b bytes.Buffer
 			manifestList, errR := r.Render(context.Background(), &b, []graph.Artifact{{ImageName: "leeroy-web", Tag: "leeroy-web:v1"}},
@@ -102,11 +99,9 @@ func TestRender(t *testing.T) {
 }
 
 type mockConfig struct {
-	renderConfig *latest.RenderConfig
-	workingDir   string
+	workingDir string
 }
 
-func (mc mockConfig) GetRenderConfig() *latest.RenderConfig       { return mc.renderConfig }
 func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
 func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
 func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
diff --git a/pkg/skaffold/render/renderer/render_mux_test.go b/pkg/skaffold/render/renderer/render_mux_test.go
new file mode 100644
index 000000000..32bde4088
--- /dev/null
+++ b/pkg/skaffold/render/renderer/render_mux_test.go
@@ -0,0 +1,101 @@
+/*
+Copyright 2022 The Skaffold 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 renderer
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/testutil"
+	testEvent "github.com/GoogleContainerTools/skaffold/testutil/event"
+)
+
+func TestRenderMux_Render(t *testing.T) {
+	tests := []struct {
+		name         string
+		renderers    []Renderer
+		expected     string
+		expectedDeps []string
+		shouldErr    bool
+	}{
+		{
+			name: "concatenates render results with separator",
+			renderers: []Renderer{
+				mock{manifests: "manifest-1", deps: []string{"file1.txt", "file2.txt"}},
+				mock{manifests: "manifest-2", deps: []string{"file2.txt", "file3.txt"}}},
+			expected:     "manifest-1\n---\nmanifest-2",
+			expectedDeps: []string{"file1.txt", "file2.txt", "file3.txt"},
+		},
+		{
+			name: "short-circuits when first call fails",
+			renderers: []Renderer{
+				mock{manifests: "manifest-1", deps: []string{"file1.txt"}},
+				mock{deps: []string{"file2.txt"}, shouldErr: true}},
+			expectedDeps: []string{"file1.txt", "file2.txt"},
+			shouldErr:    true,
+		},
+		{
+			name: "short-circuits when second call fails",
+			renderers: []Renderer{
+				mock{deps: []string{"file1.txt"}, shouldErr: true},
+				mock{manifests: "manifest-2", deps: []string{"file2.txt"}}},
+			expectedDeps: []string{"file1.txt", "file2.txt"},
+			shouldErr:    true,
+		},
+	}
+	for _, tc := range tests {
+		testutil.Run(t, tc.name, func(t *testutil.T) {
+			testEvent.InitializeState([]latest.Pipeline{{
+				Deploy: latest.DeployConfig{},
+				Build: latest.BuildConfig{
+					BuildType: latest.BuildType{
+						LocalBuild: &latest.LocalBuild{},
+					},
+				}}})
+			mux := NewRenderMux(tc.renderers)
+			buf := &bytes.Buffer{}
+			actual, err := mux.Render(context.Background(), buf, nil, true, "")
+			t.CheckErrorAndDeepEqual(tc.shouldErr, err, tc.expected, actual.String())
+			actualDeps, errD := mux.ManifestDeps()
+			t.CheckNoError(errD)
+			t.CheckDeepEqual(tc.expectedDeps, actualDeps)
+		})
+	}
+}
+
+type mock struct {
+	manifests string
+	deps      []string
+	shouldErr bool
+}
+
+func (m mock) ManifestDeps() ([]string, error) {
+	return m.deps, nil
+}
+
+func (m mock) Render(context.Context, io.Writer, []graph.Artifact, bool, string) (manifest.ManifestList, error) {
+	if m.shouldErr {
+		return nil, fmt.Errorf("render error")
+	}
+	return manifest.Load(bytes.NewReader([]byte(m.manifests)))
+}