Repository: GoogleContainerTools/skaffold
Pull request #7182 (2022-03-11 07:24:04)

fix: ignore concurrency if not specified
<!-- Thank you for your contribution! -->

<!-- Include if applicable: -->
Fixes: #7181 <!-- tracking issues that this PR will close -->

**Description**
<!-- Describe your changes here. The more detail, the easier the review! -->

The build concurrency should be set to the minimum defined concurrency across all configs. Previously, imported configs that didn't define concurrency were defaulting to the value of 1, thereby lowering the overall concurrency to 1.
We remove setting this default value in the config, rather if the resolved build concurrency is defaulted to 1 at the end.

**Testing instructions**
- In project `examples/multi-config-microservices` set the root `skaffold.yaml` config's build concurrency.
```yaml
apiVersion: skaffold/v2beta28
kind: Config
build:
  local:
    concurrency: 2
requires:
- path: ./leeroy-app
- path: ./leeroy-web
```
- Run `skaffold build -v DEBUG`. The output should have the following log lines showing that the build concurrency was updated to that value:
```
INFO[0000] build concurrency first set to 2 parsed from *local.Builder[3]  subtask=-1 task=DevLoop
INFO[0000] final build concurrency value is 2            subtask=-1 task=DevLoop
```

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7182](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (7c0b530) 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 `2.11%`.
> The diff coverage is `56.96%`.

```diff
@@            Coverage Diff             @@
##             main    #7182      +/-   ##
==========================================
- Coverage   70.48%   68.37%   -2.12%     
==========================================
  Files         515      560      +45     
  Lines       23150    26456    +3306     
==========================================
+ Hits        16317    18088    +1771     
- Misses       5776     7106    +1330     
- Partials     1057     1262     +205     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182?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/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `52.00% <ø> (-1.85%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/dev.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGV2Lmdv) | `84.61% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/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% <0.00%> (-4.72%)` | :arrow_down: |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/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/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/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/7182/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/7182/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `68.85% <40.00%> (-7.62%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lint.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/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%> (ø)` | |
| [cmd/skaffold/app/cmd/find\_configs.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZmluZF9jb25maWdzLmdv) | `48.88% <50.00%> (+0.24%)` | :arrow_up: |
| [cmd/skaffold/app/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9za2FmZm9sZC5nbw==) | `76.19% <70.00%> (-8.43%)` | :arrow_down: |
| ... and [220 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7182/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)


Patch:
diff --git a/docs/content/en/docs/environment/skaffold-resource-selector.md b/docs/content/en/docs/environment/skaffold-resource-selector.md
deleted file mode 100644
index 02273a06b..000000000
--- a/docs/content/en/docs/environment/skaffold-resource-selector.md
+++ /dev/null
@@ -1,91 +0,0 @@
----
-title: "Skaffold Resource Selector - Configure Which Resources and Which Fields In Those Resources Skaffold Manages"
-linkTitle: "Skaffold Resource Selector - Configure Which Resources and Which Fields In Those Resources Skaffold Manages"
-weight: 90
-featureId: skaffold-resource-selector
-aliases: [/docs/how-tos/skaffold-resource-selector]
----
-
-Common Use Cases This Page Helps Resolve:
-* Users who want skaffold to properly manage the rendering and deployment of their Custom CRDs (as skaffold does with K8s objects like Pod, Deployment.apps, etc.)
-  * Additionally users w/ a CRD that uses a different field name for `image:` (eg: `foo:`) and want skaffold to properly modify the value to instead have the image label for the image skaffold recently built
-* Users who are seeing issues with skaffold's default resource field overwriting for a given resource - eg: skaffold errors as it tries to mutate immutable config on re-deployment
-
-Currently skaffold modifies the manifests it renders and deploys for the following functionality:
-- status checking - done by mutating the manifest/K8s-Object by adding a label - skaffold/dev/run-id.  Skaffold uses the existance and value of this run-id to identify resources it manages
-- image label overwriting - done by mutating the manifest/K8s-Object by substituting the `image:$ORIGINAL_IMAGE_TAG` value(s) in a manifest with `image:$RECENT_SKAFFOLD_BUILT_IMAGE`
-
-
-Skaffold has by default the following resources set for management via field "labels:" and "image:" overwriting:
-
-_The below list is derived from the values defined [here](https://github.com/GoogleContainerTools/skaffold/blob/main/pkg/skaffold/kubernetes/manifest/visitor.go)_
-
-
-* Pod
-* DaemonSet.apps
-* Deployment.apps
-* ReplicaSet.apps
-* StatefulSet.apps (with the exception of `.spec.volumeClaimTemplates.*.metadata.labels` field(s))
-* CronJob.batch
-* Job.batch
-* DaemonSet.extensions
-* Deployment.extensions
-* ReplicaSet.extension
-* Service.serving.knative.dev
-* Fleet.agones.dev
-* GameServer.agones.dev
-* Rollout.argoproj.io
-* ClusterWorkflowTemplate.argoproj.io
-* Workflow.argoproj.io
-* WorkflowTemplate.argoproj.io
-* *.cnrm.cloud.google.com
-
-_This default overwriting modifies all JSON Paths for those GroupKinds of the form:_
-_* *.metadata.labels (skaffold appends a `run-id` label to existing labels or adds a `labels `field with a `run-id` entry if it didn't exist prior)_
-_* *.image (changes `image:` value to be the skaffold built image ONLY IF skaffold manages the original `image:` value)_
-
-
-The GroupKind's that Skaffold manages (via resource field overwriting) are user configurable via the `resourceSelector:` top level configuration.  The `resourceSelector` configuration allows users to modify and extend which resources and what fields of those resources skaffold modifies.  Currently skaffold only supports `label:` and `.metadata.labels` related modifications.
-
-`resourceSelector` spec (from `pkg/skaffold/schema/latest/v1/config.go`)
-```
-// ResourceSelector describes user defined filters describing how skaffold should treat objects/fields during rendering.
-ResourceSelector ResourceSelectorConfig `yaml:"resourceSelector,omitempty"`
-
-// ResourceSelectorConfig contains all the configuration needed by the deploy steps.
-type ResourceSelectorConfig struct {
-	// Allow configures an allowlist for transforming manifests.
-	Allow []ResourceFilter `yaml:"allow,omitempty"`
-	// Deny configures an allowlist for transforming manifests.
-	Deny []ResourceFilter `yaml:"deny,omitempty"`
-}
-
-// ResourceFilter contains definition to filter which resource to transform.
-type ResourceFilter struct {
-	// GroupKind is the compact format of a resource type.
-	GroupKind string `yaml:"groupKind" yamltags:"required"`
-	// Image is an optional slice of JSON-path-like paths of where to rewrite images.
-	Image []string `yaml:"image,omitempty"`
-	// Labels is an optional slide of JSON-path-like paths of where to add a labels block if missing.
-	Labels []string `yaml:"labels,omitempty"`
-}
-```
-
-The values for `Image` and `Labels` support a JSON Path style string which designates a path to a field in the speified GroupKind.  Additionally there is a special `.*` value that can be used which means that skaffold will attempt to overwrite all relevant labels following the below rules:
-- image: [".*"] -> replace all fields which follow `*.image:` where the value is an image that skaffold manages/builds
-- labels: [".*"] -> append-to or create a field named `*.metadata.labels` if a field `*.metadata` is found
-
-Some example use cases and motivations for the `resourceSelector` are shown below:
-* Skaffold Management of Custom CRD - The below snippet using `resourceSelector` allows a user to configure skaffold to manage a custom CRD (eg: CustomDeployment.skaffold.dev) they've created for their application in a skaffold.  
-
-_Without this snippet, skaffold would apply the yaml but would not properly wait for child resources or replace the `image:` values with skaffold built images_
-{{% readfile file="samples/resource-selector/resource-selector-crd-example.yaml" %}}
-
-Using the above configuation, skaffold will properly update any `*.image` field and `*.metadata.labels` field allowing it to work as expected (similar to Pod, Deployment.apps, etc.)
-
-* Fix Issue With Skaffold Overwriting Immutable Field - The below snippet using `resourceSelector` shows a user configuring skaffold to change it's behaviour to NOT overwrite a resource's field to prevent K8s errors related to overwriting an immutable field:
-_The below configuration is actually a part of skaffold's default configuration, just made into a snippet to use as an example_
-{{% readfile file="samples/resource-selector/resource-selector-deny-example.yaml" %}}
-
-* Allow `image:` Overwriting For Differently Named Image Field(s) - The below snippet using `resourceSelector` shows a user configuring skaffold to change it's behaviour to overwrite a resource's `foo:`field with skaffold built images.  This allows skaffold to properly support the images skaffold builds this resources which uses `foo:` instead of `image:` for an image value:
-{{% readfile file="samples/resource-selector/resource-selector-allow-example.yaml" %}}
diff --git a/docs/content/en/docs/pipeline-stages/port-forwarding.md b/docs/content/en/docs/pipeline-stages/port-forwarding.md
index 1d5dddc45..ff808a29e 100644
--- a/docs/content/en/docs/pipeline-stages/port-forwarding.md
+++ b/docs/content/en/docs/pipeline-stages/port-forwarding.md
@@ -69,7 +69,7 @@ User-defined port-forwards in the `skaffold.yaml` are unaffected and can bind to
 {{< /alert >}}
 
 {{< alert title="Note about user-defined port-forwarding for Docker deployments" >}}
-When [deploying to Docker]({{< relref "/pipeline-stages/deployers/docker" >}}) with a user-defined port-forward in the `skaffold.yaml`, the `resourceType` of `portForward` must be set to `container`. Otherwise, Skaffold will not tell the Docker daemon to expose that port.
+When [deploying to Docker]({{< relref "/docs/pipeline-stages/deployers/docker" >}}) with a user-defined port-forward in the `skaffold.yaml`, the `resourceType` of `portForward` must be set to `container`. Otherwise, Skaffold will not tell the Docker daemon to expose that port.
 {{< /alert >}}
 
 Skaffold will run `kubectl port-forward` on each of these resources in addition to the automatic port forwarding described above.
diff --git a/docs/content/en/docs/tutorials/skaffold-resource-selector.md b/docs/content/en/docs/tutorials/skaffold-resource-selector.md
index f805ed638..000779257 100644
--- a/docs/content/en/docs/tutorials/skaffold-resource-selector.md
+++ b/docs/content/en/docs/tutorials/skaffold-resource-selector.md
@@ -1,13 +1,13 @@
 ---
-title: "Skaffold Resource Selector - Configure Which Resources and Which Fields In Those Resources Skaffold Manages"
-linkTitle: "Skaffold Resource Selector - Configure Which Resources and Which Fields In Those Resources Skaffold Manages"
+title: "Manage CRDs w/ Skaffold - Configuring Which K8s Resources & Fields Skaffold Manages"
+linkTitle: "Manage CRDs w/ Skaffold - Configuring Which K8s Resources & Fields Skaffold Manages"
 weight: 90
 featureId: skaffold-resource-selector
 aliases: [/docs/how-tos/skaffold-resource-selector]
 ---
 
 Common Use Cases This Page Helps Resolve:
-* Users who want skaffold to properly manage the rendering and deployment of their Custom CRDs (as skaffold does with K8s objects like Pod, Deployment.apps, etc.)
+* Users who want skaffold to properly manage the rendering and deployment of their custom CRDs (as skaffold does with K8s objects like Pod, Deployment.apps, etc.)
   * Additionally users w/ a CRD that uses a different field name for `image:` (eg: `foo:`) and want skaffold to properly modify the value to instead have the image label for the image skaffold recently built
 * Users who are seeing issues with skaffold's default resource field overwriting for a given resource - eg: skaffold errors as it tries to mutate immutable config on re-deployment
 
diff --git a/docs/static/javascripts/tabs.js b/docs/static/javascripts/tabs.js
index 065602226..95dd67085 100644
--- a/docs/static/javascripts/tabs.js
+++ b/docs/static/javascripts/tabs.js
@@ -1,6 +1,6 @@
 function initTabs() {
-    $('.tab-content').find('.tab-pane').each(function(idx, item) {
-      var navTabs = $(this).closest('.code-tabs').find('.nav-tabs'),
+    $('.tab-content').children('.tab-pane').each(function(idx, item) {
+      var navTabs = $(this).closest('.code-tabs').children('.nav-tabs'),
           title = $(this).attr('title');
       navTabs.append('<li class="nav-tab"><a href="#" class="nav-tab">'+title+'</a></li');
     });
@@ -18,9 +18,10 @@ function initTabs() {
       var tab = $(this).parent(),
           tabIndex = tab.index(),
           tabPanel = $(this).closest('.code-tabs'),
-          tabPane = tabPanel.find('.tab-pane').eq(tabIndex);
-      tabPanel.find('.active').removeClass('active');
+          tabPane = tabPanel.find('.tab-content:first').children('.tab-pane').eq(tabIndex);
+      tab.siblings().removeClass('active');
+      tabPane.siblings().removeClass('active');
       tab.addClass('active');
       tabPane.addClass('active');
     });
-  }
\ No newline at end of file
+  }
diff --git a/pkg/skaffold/build/build.go b/pkg/skaffold/build/build.go
index 414ed06fb..676f0400a 100644
--- a/pkg/skaffold/build/build.go
+++ b/pkg/skaffold/build/build.go
@@ -51,7 +51,7 @@ type PipelineBuilder interface {
 	PostBuild(ctx context.Context, out io.Writer) error
 
 	// Concurrency specifies the max number of builds that can run at any one time. If concurrency is 0, then all builds can run in parallel.
-	Concurrency() int
+	Concurrency() *int
 
 	// Prune removes images built in this pipeline
 	Prune(context.Context, io.Writer) error
diff --git a/pkg/skaffold/build/builder_mux.go b/pkg/skaffold/build/builder_mux.go
index ab7853e20..c079acaa1 100644
--- a/pkg/skaffold/build/builder_mux.go
+++ b/pkg/skaffold/build/builder_mux.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"reflect"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
@@ -51,38 +52,19 @@ type Config interface {
 func NewBuilderMux(cfg Config, store ArtifactStore, builder func(p latestV1.Pipeline) (PipelineBuilder, error)) (*BuilderMux, error) {
 	pipelines := cfg.GetPipelines()
 	m := make(map[string]PipelineBuilder)
-	var pb []PipelineBuilder
-	minConcurrency := -1
-	for i, p := range pipelines {
+	var pbs []PipelineBuilder
+	for _, p := range pipelines {
 		b, err := builder(p)
 		if err != nil {
 			return nil, fmt.Errorf("creating builder: %w", err)
 		}
-		pb = append(pb, b)
+		pbs = append(pbs, b)
 		for _, a := range p.Build.Artifacts {
 			m[a.ImageName] = b
 		}
-
-		if cfg.BuildConcurrency() >= 0 {
-			minConcurrency = cfg.BuildConcurrency()
-		} else {
-			concurrency := b.Concurrency()
-			// set mux concurrency to be the minimum of all builders' concurrency. (concurrency = 0 means unlimited)
-			switch {
-			case minConcurrency < 0:
-				minConcurrency = concurrency
-				log.Entry(context.TODO()).Infof("build concurrency first set to %d parsed from %s[%d]", minConcurrency, reflect.TypeOf(b).String(), i)
-			case concurrency > 0 && (minConcurrency == 0 || concurrency < minConcurrency):
-				minConcurrency = concurrency
-				log.Entry(context.TODO()).Infof("build concurrency updated to %d parsed from %s[%d]", minConcurrency, reflect.TypeOf(b).String(), i)
-			default:
-				log.Entry(context.TODO()).Infof("build concurrency value %d parsed from %s[%d] is ignored since it's not less than previously set value %d", concurrency, reflect.TypeOf(b).String(), i, minConcurrency)
-			}
-		}
 	}
-	log.Entry(context.TODO()).Infof("final build concurrency value is %d", minConcurrency)
-
-	return &BuilderMux{builders: pb, byImageName: m, store: store, concurrency: minConcurrency}, nil
+	concurrency := getConcurrency(pbs, cfg.BuildConcurrency())
+	return &BuilderMux{builders: pbs, byImageName: m, store: store, concurrency: concurrency}, nil
 }
 
 // Build executes the specific image builder for each artifact in the given artifact slice.
@@ -98,7 +80,7 @@ func (b *BuilderMux) Build(ctx context.Context, out io.Writer, tags tag.ImageTag
 		}
 	}
 
-	builder := func(ctx context.Context, out io.Writer, artifact *latestV1.Artifact, tag string, platforms platform.Matcher) (string, error) {
+	builderF := func(ctx context.Context, out io.Writer, artifact *latestV1.Artifact, tag string, platforms platform.Matcher) (string, error) {
 		p := b.byImageName[artifact.ImageName]
 		pl, err := filterBuildEnvSupportedPlatforms(p.SupportedPlatforms(), platforms)
 		if err != nil {
@@ -124,14 +106,14 @@ func (b *BuilderMux) Build(ctx context.Context, out io.Writer, tags tag.ImageTag
 		}
 		return built, nil
 	}
-	ar, err := InOrder(ctx, out, tags, resolver, artifacts, builder, b.concurrency, b.store)
+	ar, err := InOrder(ctx, out, tags, resolver, artifacts, builderF, b.concurrency, b.store)
 	if err != nil {
 		return nil, err
 	}
 
 	for builder := range m {
-		if err := builder.PostBuild(ctx, out); err != nil {
-			return nil, err
+		if errB := builder.PostBuild(ctx, out); errB != nil {
+			return nil, errB
 		}
 	}
 
@@ -159,3 +141,36 @@ func filterBuildEnvSupportedPlatforms(supported platform.Matcher, target platfor
 	}
 	return pl, nil
 }
+
+func getConcurrency(pbs []PipelineBuilder, cliConcurrency int) int {
+	ctx := context.TODO()
+	if cliConcurrency >= 0 {
+		log.Entry(ctx).Infof("build concurrency set to cli concurrency %d", cliConcurrency)
+		return cliConcurrency
+	}
+	minConcurrency := -1
+	for i, b := range pbs {
+		concurrency := 1
+		if b.Concurrency() != nil {
+			concurrency = *b.Concurrency()
+		}
+		// set mux concurrency to be the minimum of all builders' concurrency. (concurrency = 0 means unlimited)
+		switch {
+		case minConcurrency < 0:
+			minConcurrency = concurrency
+			log.Entry(ctx).Infof("build concurrency first set to %d parsed from %s[%d]", minConcurrency, reflect.TypeOf(b).String(), i)
+		case concurrency > 0 && (minConcurrency == 0 || concurrency < minConcurrency):
+			minConcurrency = concurrency
+			log.Entry(ctx).Infof("build concurrency updated to %d parsed from %s[%d]", minConcurrency, reflect.TypeOf(b).String(), i)
+		default:
+			log.Entry(ctx).Infof("build concurrency value %d parsed from %s[%d] is ignored since it's not less than previously set value %d", concurrency, reflect.TypeOf(b).String(), i, minConcurrency)
+		}
+	}
+	if minConcurrency < 0 {
+		log.Entry(ctx).Infof("build concurrency set to default value of %d", minConcurrency)
+		// set default concurrency to 1 for local builder. For GCB and Cluster build the default value is 0
+		return constants.DefaultLocalConcurrency
+	}
+	log.Entry(ctx).Infof("final build concurrency value is %d", minConcurrency)
+	return minConcurrency
+}
diff --git a/pkg/skaffold/build/cluster/cluster.go b/pkg/skaffold/build/cluster/cluster.go
index c265307d8..242abf2ab 100644
--- a/pkg/skaffold/build/cluster/cluster.go
+++ b/pkg/skaffold/build/cluster/cluster.go
@@ -70,8 +70,8 @@ func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, artifact *la
 	return build.TagWithDigest(tag, digest), nil
 }
 
-func (b *Builder) Concurrency() int {
-	return b.ClusterDetails.Concurrency
+func (b *Builder) Concurrency() *int {
+	return util.IntPtr(b.ClusterDetails.Concurrency)
 }
 
 func (b *Builder) runBuildForArtifact(ctx context.Context, out io.Writer, a *latestV1.Artifact, tag string, platforms platform.Matcher) (string, error) {
diff --git a/pkg/skaffold/build/gcb/cloud_build.go b/pkg/skaffold/build/gcb/cloud_build.go
index 383dbd920..ce69b616e 100644
--- a/pkg/skaffold/build/gcb/cloud_build.go
+++ b/pkg/skaffold/build/gcb/cloud_build.go
@@ -44,6 +44,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
 	latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sources"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 	"github.com/GoogleContainerTools/skaffold/proto/v1"
 )
 
@@ -65,8 +66,8 @@ func (b *Builder) PostBuild(_ context.Context, _ io.Writer) error {
 	return nil
 }
 
-func (b *Builder) Concurrency() int {
-	return b.GoogleCloudBuild.Concurrency
+func (b *Builder) Concurrency() *int {
+	return util.IntPtr(b.GoogleCloudBuild.Concurrency)
 }
 
 func (b *Builder) buildArtifactWithCloudBuild(ctx context.Context, out io.Writer, artifact *latestV1.Artifact, tag string, platform platform.Matcher) (string, error) {
diff --git a/pkg/skaffold/build/local/local.go b/pkg/skaffold/build/local/local.go
index df70a42ab..bbf40f327 100644
--- a/pkg/skaffold/build/local/local.go
+++ b/pkg/skaffold/build/local/local.go
@@ -58,12 +58,7 @@ func (b *Builder) PostBuild(ctx context.Context, _ io.Writer) error {
 	return nil
 }
 
-func (b *Builder) Concurrency() int {
-	if b.local.Concurrency == nil {
-		return 0
-	}
-	return *b.local.Concurrency
-}
+func (b *Builder) Concurrency() *int { return b.local.Concurrency }
 
 func (b *Builder) PushImages() bool {
 	return b.pushImages
diff --git a/pkg/skaffold/deploy/util/util.go b/pkg/skaffold/deploy/util/util.go
index a7ef02b7b..5c5a95fff 100644
--- a/pkg/skaffold/deploy/util/util.go
+++ b/pkg/skaffold/deploy/util/util.go
@@ -30,6 +30,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/types"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
 	latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1"
@@ -123,11 +124,13 @@ func ConsolidateTransformConfiguration(cfg types.Config) (map[schema.GroupKind]l
 
 	// add user schema values, override defaults
 	for _, rf := range cfg.TransformAllowList() {
+		instrumentation.AddResourceFilter("schema", "allow")
 		groupKind := schema.ParseGroupKind(rf.GroupKind)
 		transformableAllowlist[groupKind] = convertJSONPathIndex(rf)
 		delete(transformableDenylist, groupKind)
 	}
 	for _, rf := range cfg.TransformDenyList() {
+		instrumentation.AddResourceFilter("schema", "deny")
 		groupKind := schema.ParseGroupKind(rf.GroupKind)
 		transformableDenylist[groupKind] = convertJSONPathIndex(rf)
 		delete(transformableAllowlist, groupKind)
@@ -146,12 +149,14 @@ func ConsolidateTransformConfiguration(cfg types.Config) (map[schema.GroupKind]l
 			return nil, nil, err
 		}
 		for _, rf := range rsc.Allow {
+			instrumentation.AddResourceFilter("cli-flag", "allow")
 			groupKind := schema.ParseGroupKind(rf.GroupKind)
 			transformableAllowlist[groupKind] = convertJSONPathIndex(rf)
 			delete(transformableDenylist, groupKind)
 		}
 
 		for _, rf := range rsc.Deny {
+			instrumentation.AddResourceFilter("cli-flag", "deny")
 			groupKind := schema.ParseGroupKind(rf.GroupKind)
 			transformableDenylist[groupKind] = convertJSONPathIndex(rf)
 			delete(transformableAllowlist, groupKind)
diff --git a/pkg/skaffold/instrumentation/export.go b/pkg/skaffold/instrumentation/export.go
index d9c313478..c86b11fb6 100644
--- a/pkg/skaffold/instrumentation/export.go
+++ b/pkg/skaffold/instrumentation/export.go
@@ -204,6 +204,9 @@ func createMetrics(ctx context.Context, meter skaffoldMeter) {
 		if doesDeploy.Contains(meter.Command) {
 			deployerMetrics(ctx, meter, m, sharedLabels...)
 		}
+		if doesDeploy.Contains(meter.Command) || meter.Command == "render" {
+			resourceSelectorMetrics(ctx, meter, m, sharedLabels...)
+		}
 	}
 
 	if meter.ErrorCode != 0 {
@@ -267,6 +270,15 @@ func deployerMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, l
 	}
 }
 
+func resourceSelectorMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, labels ...attribute.KeyValue) {
+	if len(meter.ResourceFilters) > 0 {
+		resourceFilters := metric.Must(m).NewInt64ValueRecorder("resource-filters", metric.WithDescription("The resource filters defined for rendering and/or deployment"))
+		for _, resourceFilter := range meter.ResourceFilters {
+			resourceFilters.Record(ctx, 1, append(labels, attribute.String("source", resourceFilter.Source), attribute.String("type", resourceFilter.Type))...)
+		}
+	}
+}
+
 func builderMetrics(ctx context.Context, meter skaffoldMeter, m metric.Meter, labels ...attribute.KeyValue) {
 	builderCounter := metric.Must(m).NewInt64ValueRecorder("builders", metric.WithDescription("Builders used"))
 	artifactCounter := metric.Must(m).NewInt64ValueRecorder("artifacts", metric.WithDescription("Number of artifacts used"))
diff --git a/pkg/skaffold/instrumentation/meter.go b/pkg/skaffold/instrumentation/meter.go
index 2a114161c..fe8e74ffa 100644
--- a/pkg/skaffold/instrumentation/meter.go
+++ b/pkg/skaffold/instrumentation/meter.go
@@ -143,6 +143,10 @@ func AddDevIterationErr(i int, errorCode proto.StatusCode) {
 	meter.DevIterations[i].ErrorCode = errorCode
 }
 
+func AddResourceFilter(source string, typ string) {
+	meter.ResourceFilters = append(meter.ResourceFilters, resourceFilter{Source: source, Type: typ})
+}
+
 func AddFlag(flag *flag.Flag) {
 	if flag.Changed {
 		meter.EnumFlags[flag.Name] = flag.Value.String()
diff --git a/pkg/skaffold/instrumentation/types.go b/pkg/skaffold/instrumentation/types.go
index 507066ccf..bb80fa7bc 100644
--- a/pkg/skaffold/instrumentation/types.go
+++ b/pkg/skaffold/instrumentation/types.go
@@ -83,6 +83,9 @@ type skaffoldMeter struct {
 	// reasons they were triggered. The triggers can be one of sync, build, or deploy.
 	DevIterations []devIteration
 
+	// ResourceFilters represents user defined resource filters including 'source' and 'type' information
+	ResourceFilters []resourceFilter
+
 	// StartTime Start time of the Skaffold program, used to track how long Skaffold took to finish executing.
 	StartTime time.Time
 
@@ -115,6 +118,15 @@ type devIteration struct {
 	ErrorCode proto.StatusCode
 }
 
+// resourceFilter describes a user defined resource filter.
+type resourceFilter struct {
+	// Source is the source configuration of the resource filter (cli-flag, schema)
+	Source string
+
+	// Type is the defined type of the resource filter (allow, deny)
+	Type string
+}
+
 // creds contains the Google Cloud project ID.
 type creds struct {
 	// ProjectID is the ID of the Google Cloud project to upload metrics to.
diff --git a/pkg/skaffold/schema/defaults/defaults.go b/pkg/skaffold/schema/defaults/defaults.go
index c14aa8337..672e04f8d 100644
--- a/pkg/skaffold/schema/defaults/defaults.go
+++ b/pkg/skaffold/schema/defaults/defaults.go
@@ -76,13 +76,6 @@ func Set(c *latestV1.SkaffoldConfig) error {
 		}
 	}
 
-	withLocalBuild(c, func(lb *latestV1.LocalBuild) {
-		// don't set build concurrency if there are no artifacts in the current config
-		if len(c.Build.Artifacts) > 0 {
-			setDefaultConcurrency(lb)
-		}
-	})
-
 	withCloudBuildConfig(c,
 		setDefaultCloudBuildDockerImage,
 		setDefaultCloudBuildMavenImage,
@@ -136,20 +129,6 @@ func defaultToKubectlDeploy(c *latestV1.SkaffoldConfig) {
 	c.Deploy.DeployType.KubectlDeploy = &latestV1.KubectlDeploy{}
 }
 
-func withLocalBuild(c *latestV1.SkaffoldConfig, operations ...func(*latestV1.LocalBuild)) {
-	if local := c.Build.LocalBuild; local != nil {
-		for _, operation := range operations {
-			operation(local)
-		}
-	}
-}
-
-func setDefaultConcurrency(local *latestV1.LocalBuild) {
-	if local.Concurrency == nil {
-		local.Concurrency = &constants.DefaultLocalConcurrency
-	}
-}
-
 func withCloudBuildConfig(c *latestV1.SkaffoldConfig, operations ...func(*latestV1.GoogleCloudBuild)) {
 	if gcb := c.Build.GoogleCloudBuild; gcb != nil {
 		for _, operation := range operations {


Test patch:
diff --git a/integration/testdata/diagnose/multi-config/diagnose.tmpl b/integration/testdata/diagnose/multi-config/diagnose.tmpl
index d6e64e61f..b3d020563 100644
--- a/integration/testdata/diagnose/multi-config/diagnose.tmpl
+++ b/integration/testdata/diagnose/multi-config/diagnose.tmpl
@@ -10,8 +10,7 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  local:
-    concurrency: 1
+  local: {}
 deploy:
   kubectl:
     manifests:
@@ -31,8 +30,7 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  local:
-    concurrency: 1
+  local: {}
 deploy:
   kubectl:
     manifests:
@@ -50,8 +48,7 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  local:
-    concurrency: 1
+  local: {}
 deploy:
   kubectl:
     manifests:
diff --git a/integration/testdata/diagnose/temp-config/diagnose.tmpl b/integration/testdata/diagnose/temp-config/diagnose.tmpl
index 5dbba2eb2..278c618cb 100644
--- a/integration/testdata/diagnose/temp-config/diagnose.tmpl
+++ b/integration/testdata/diagnose/temp-config/diagnose.tmpl
@@ -8,8 +8,7 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  local:
-    concurrency: 1
+  local: {}
 deploy:
   kubectl:
     manifests:
diff --git a/pkg/skaffold/build/builder_mux_test.go b/pkg/skaffold/build/builder_mux_test.go
index 49516ff8c..f79b17d98 100644
--- a/pkg/skaffold/build/builder_mux_test.go
+++ b/pkg/skaffold/build/builder_mux_test.go
@@ -92,6 +92,93 @@ func TestNewBuilderMux(t *testing.T) {
 	}
 }
 
+func TestGetConcurrency(t *testing.T) {
+	tests := []struct {
+		description         string
+		pbs                 []PipelineBuilder
+		cliConcurrency      int
+		expectedConcurrency int
+	}{
+		{
+			description: "default concurrency - builder and cli concurrency unset.",
+			pbs: []PipelineBuilder{
+				&mockPipelineBuilder{concurrency: nil, builderType: "local"},
+				&mockPipelineBuilder{concurrency: nil, builderType: "gcb"},
+			},
+			cliConcurrency:      -1,
+			expectedConcurrency: 1,
+		},
+		{
+			description: "builder concurrency set to less than cli concurrency",
+			pbs: []PipelineBuilder{
+				&mockPipelineBuilder{concurrency: util.IntPtr(1), builderType: "local"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(1), builderType: "local"},
+				&mockPipelineBuilder{concurrency: nil, builderType: "gcb"},
+			},
+			cliConcurrency:      2,
+			expectedConcurrency: 2,
+		},
+		{
+			description: "builder concurrency set",
+			pbs: []PipelineBuilder{
+				&mockPipelineBuilder{concurrency: util.IntPtr(2), builderType: "local"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(2), builderType: "local"},
+				// As per docs https://github.com/GoogleContainerTools/skaffold/blob/dbd18994955f5805e80c6354ed0fd424ec4d987b/pkg/skaffold/schema/v2beta26/config.go#L287
+				// nil concurrency defaults to 1
+				&mockPipelineBuilder{concurrency: nil, builderType: "local"},
+			},
+			cliConcurrency:      -1,
+			expectedConcurrency: 1,
+		},
+		{
+			description: "builder concurrency set to 0 and cli concurrency set to 1",
+			pbs: []PipelineBuilder{
+				// build all in parallel
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "local"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "gcb"},
+			},
+			cliConcurrency:      1,
+			expectedConcurrency: 1,
+		},
+		{
+			description: "builder concurrency set to 0 and cli concurrency unset",
+			pbs: []PipelineBuilder{
+				// build all in parallel
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "local"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "gcb"},
+			},
+			cliConcurrency:      -1,
+			expectedConcurrency: 0,
+		},
+		{
+			description: "builder concurrency set to default 0 for gcb",
+			pbs: []PipelineBuilder{
+				// build all in parallel
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "gcb"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "gcb"},
+			},
+			cliConcurrency:      -1,
+			expectedConcurrency: 0,
+		},
+		{
+			description: "min non-zero concurrency",
+			pbs: []PipelineBuilder{
+				&mockPipelineBuilder{concurrency: util.IntPtr(0), builderType: "local"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(3), builderType: "gcb"},
+				&mockPipelineBuilder{concurrency: util.IntPtr(2), builderType: "gcb"},
+			},
+			cliConcurrency:      -1,
+			expectedConcurrency: 2,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			actual := getConcurrency(test.pbs, test.cliConcurrency)
+			t.CheckDeepEqual(test.expectedConcurrency, actual)
+		})
+	}
+}
+
 type mockConfig struct {
 	pipelines []latestV1.Pipeline
 	optRepo   string
@@ -109,7 +196,7 @@ func (m *mockConfig) MultiLevelRepo() *bool { return nil }
 func (m *mockConfig) BuildConcurrency() int { return -1 }
 
 type mockPipelineBuilder struct {
-	concurrency int
+	concurrency *int
 	builderType string
 }
 
@@ -121,7 +208,7 @@ func (m *mockPipelineBuilder) Build(ctx context.Context, out io.Writer, artifact
 
 func (m *mockPipelineBuilder) PostBuild(ctx context.Context, out io.Writer) error { return nil }
 
-func (m *mockPipelineBuilder) Concurrency() int { return m.concurrency }
+func (m *mockPipelineBuilder) Concurrency() *int { return m.concurrency }
 
 func (m *mockPipelineBuilder) Prune(context.Context, io.Writer) error { return nil }
 
@@ -132,15 +219,11 @@ func (m *mockPipelineBuilder) SupportedPlatforms() platform.Matcher { return pla
 func newMockPipelineBuilder(p latestV1.Pipeline) (PipelineBuilder, error) {
 	switch {
 	case p.Build.BuildType.LocalBuild != nil:
-		c := 0
-		if p.Build.LocalBuild.Concurrency != nil {
-			c = *p.Build.LocalBuild.Concurrency
-		}
-		return &mockPipelineBuilder{builderType: "local", concurrency: c}, nil
+		return &mockPipelineBuilder{builderType: "local", concurrency: p.Build.LocalBuild.Concurrency}, nil
 	case p.Build.BuildType.Cluster != nil:
-		return &mockPipelineBuilder{builderType: "cluster", concurrency: p.Build.Cluster.Concurrency}, nil
+		return &mockPipelineBuilder{builderType: "cluster", concurrency: util.IntPtr(p.Build.Cluster.Concurrency)}, nil
 	case p.Build.BuildType.GoogleCloudBuild != nil:
-		return &mockPipelineBuilder{builderType: "gcb", concurrency: p.Build.GoogleCloudBuild.Concurrency}, nil
+		return &mockPipelineBuilder{builderType: "gcb", concurrency: util.IntPtr(p.Build.GoogleCloudBuild.Concurrency)}, nil
 	default:
 		return nil, errors.New("invalid config")
 	}
diff --git a/pkg/skaffold/instrumentation/export_test.go b/pkg/skaffold/instrumentation/export_test.go
index 3a32eca37..b90677820 100644
--- a/pkg/skaffold/instrumentation/export_test.go
+++ b/pkg/skaffold/instrumentation/export_test.go
@@ -81,6 +81,7 @@ func TestExportMetrics(t *testing.T) {
 		ResolvedBuildTargetPlatforms: []string{"linux/amd64"},
 		HelmReleasesCount:            2,
 		DevIterations:                []devIteration{{"sync", 0}, {"build", 400}, {"build", 0}, {"sync", 200}, {"deploy", 0}},
+		ResourceFilters:              []resourceFilter{{"schema", "allow"}, {"schema", "deny"}, {"cli-flag", "allow"}, {"cli-flag", "deny"}},
 		ErrorCode:                    proto.StatusCode_BUILD_CANCELLED,
 		StartTime:                    startTime.Add(time.Hour * 24 * 30),
 		Duration:                     time.Minute * 2,
@@ -404,6 +405,7 @@ func checkOutput(t *testutil.T, meters []skaffoldMeter, b []byte) {
 	buildDeps := make(map[interface{}]int)
 	helmReleases := make(map[interface{}]int)
 	devIterations := make(map[interface{}]int)
+	resourceFilters := make(map[interface{}]int)
 	deployers := make(map[interface{}]int)
 	enumFlags := make(map[interface{}]int)
 	platform := make(map[interface{}]int)
@@ -412,7 +414,7 @@ func checkOutput(t *testutil.T, meters []skaffoldMeter, b []byte) {
 	nodePlatforms := make(map[interface{}]int)
 
 	testMaps := []map[interface{}]int{
-		platform, osCount, versionCount, archCount, durationCount, commandCount, errorCount, builders, devIterations, deployers}
+		platform, osCount, versionCount, archCount, durationCount, commandCount, errorCount, builders, devIterations, resourceFilters, deployers}
 
 	for _, meter := range meters {
 		osCount[meter.OS]++
@@ -449,6 +451,11 @@ func checkOutput(t *testutil.T, meters []skaffoldMeter, b []byte) {
 				deployers[d]++
 			}
 		}
+		if doesDeploy.Contains(meter.Command) || meter.Command == "render" {
+			for _, filterI := range meter.ResourceFilters {
+				resourceFilters[filterI]++
+			}
+		}
 	}
 
 	var lines []*line
@@ -481,6 +488,8 @@ func checkOutput(t *testutil.T, meters []skaffoldMeter, b []byte) {
 		case "dev/iterations", "debug/iterations":
 			e := l.Labels["error"]
 			devIterations[devIteration{l.Labels["intent"], proto.StatusCode(proto.StatusCode_value[e])}]--
+		case "resource-filters":
+			resourceFilters[resourceFilter{l.Labels["source"], l.Labels["type"]}]--
 		case "errors":
 			e := l.Labels["error"]
 			errorCount[e]--
diff --git a/pkg/skaffold/parser/config_test.go b/pkg/skaffold/parser/config_test.go
index bd9c33854..913a723cb 100644
--- a/pkg/skaffold/parser/config_test.go
+++ b/pkg/skaffold/parser/config_test.go
@@ -70,16 +70,11 @@ func createCfg(name string, imageName string, workspace string, requires []lates
 			Artifacts: []*latestV1.Artifact{{ImageName: imageName, ArtifactType: latestV1.ArtifactType{
 				DockerArtifact: &latestV1.DockerArtifact{DockerfilePath: "Dockerfile"}}, Workspace: workspace}}, TagPolicy: latestV1.TagPolicy{
 				GitTagger: &latestV1.GitTagger{}}, BuildType: latestV1.BuildType{
-				LocalBuild: &latestV1.LocalBuild{Concurrency: concurrency()},
+				LocalBuild: &latestV1.LocalBuild{},
 			}}, Deploy: latestV1.DeployConfig{Logs: latestV1.LogsConfig{Prefix: "container"}}},
 	}
 }
 
-func concurrency() *int {
-	c := 1
-	return &c
-}
-
 type document struct {
 	path    string
 	configs []mockCfg
diff --git a/pkg/skaffold/schema/defaults/defaults_test.go b/pkg/skaffold/schema/defaults/defaults_test.go
index a34d178be..94e85f425 100644
--- a/pkg/skaffold/schema/defaults/defaults_test.go
+++ b/pkg/skaffold/schema/defaults/defaults_test.go
@@ -319,7 +319,6 @@ func TestSetDefaultsOnLocalBuild(t *testing.T) {
 	err = Set(cfg2)
 	testutil.CheckError(t, false, err)
 	SetDefaultDeployer(cfg2)
-	testutil.CheckDeepEqual(t, 1, *cfg2.Build.LocalBuild.Concurrency)
 }
 
 func TestSetPortForwardLocalPort(t *testing.T) {
diff --git a/pkg/skaffold/schema/versions_test.go b/pkg/skaffold/schema/versions_test.go
index 03a2a5485..ce03cc9dd 100644
--- a/pkg/skaffold/schema/versions_test.go
+++ b/pkg/skaffold/schema/versions_test.go
@@ -493,9 +493,6 @@ func withLocalBuild(ops ...func(*latestV1.BuildConfig)) func(*latestV1.SkaffoldC
 		for _, op := range ops {
 			op(&b)
 		}
-		if len(b.Artifacts) > 0 {
-			b.LocalBuild.Concurrency = &constants.DefaultLocalConcurrency
-		}
 		cfg.Build = b
 	}
 }