Repository: rancher/fleet

Issue:
Fleet adds `ignore: {}` in `HelmOp.spec.targets`
### Is there an existing issue for this?

- [x] I have searched the existing issues

### Current Behavior

When creating new HelmOps, Fleet adds the unknown property `ignore: {}` in targets field.
This was found during UI tests: https://github.com/rancher/dashboard/pull/14525#issuecomment-2990120989

### Expected Behavior

_No response_

### Steps To Reproduce

_No response_

### Environment

```markdown
- Architecture:
- Fleet Version: v0.13.0-alpha.6
- Cluster:
  - Provider:
  - Options:
  - Kubernetes Version:
```

### Logs

```markdown

```

### Anything else?

_No response_

Discussion:
@weyfonk: ## Additional QA

### Problem
Fleet would add an `ignore` field to HelmOps' (and bundle deployments') specs, keeping that field visible even if empty.
This would cause UI issues.

### Solution
Made that field a pointer and ensured its JSON tags were properly defined, enabling it to be masked when empty.

### Testing

### Engineering Testing
#### Manual Testing
1. Created a HelmOp without any ignore options
2. Observed that `ignore: {}` would appear in its spec root and targets
3. Added the fix
4. Rebuilt and reinstalled Fleet
5. Repeated 1., and observed that `ignore: {}` had disappeared.
 
#### Automated Testing
N/A

### QA Testing Considerations
When creating a HelmOp without any ignore options, no `ignore: {}` should be visible in its YAML.
 
#### Regressions Considerations
When creating a HelmOp with non-empty ignore options in its targets, the `ignore` field should be visible, containing the specified config.@mmartin24: Verified in Rancher `v2.12-8749d10b2528acaf2a76a8a0db91b70edc8e5271-head` with Fleet `107.0.0+up0.13.0-beta.2`

----

Adding helm ops file without `ignore {}` correctly skips the value in the result yaml in spec root and targets parts:

To test it I used:

```yaml
apiVersion: fleet.cattle.io/v1alpha1
kind: HelmOp
metadata:
  name: graphana-test
  namespace: fleet-local
spec:
  correctDrift:
    enabled: false
  helm:
    chart: grafana
    releaseName: grafana
    repo: https://grafana.github.io/helm-charts
    version: 9.2.8
  targets:
    - clusterSelector:
        matchExpressions:
          - key: provider.cattle.io
            operator: NotIn
            values:
              - harvester
 ```` 
As can be seen in screenshot no traces of `ignore {} ` later in the resulting yaml of the repo:
![Image](https://github.com/user-attachments/assets/c5e10825-d587-4de7-a8fb-3ec1614fe756)

Solution patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d3bcaf13..5673672c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -151,7 +151,11 @@ jobs:
       - name: Attest provenance
         shell: bash
         run: |
-          for IMG_NAME in $(yq e '.dockers[].image_templates[0]' .goreleaser.yaml | grep STAGE_REGISTRY | sed "s/{{ .Env.STAGE_REGISTRY }}/${{ env.STAGE_REGISTRY }}/g" | sed "s/{{ .Tag }}/${{ github.ref_name }}/g"); do
+          ARCH_IMAGES=$(yq e '.dockers[].image_templates[0]' .goreleaser.yaml | grep STAGE_REGISTRY | sed "s/{{ .Env.STAGE_REGISTRY }}/${{ env.STAGE_REGISTRY }}/g" | sed "s/{{ .Tag }}/${{ github.ref_name }}/g")
+
+          MANIFESTS=$(echo "$ARCH_IMAGES" | sed 's/-linux-[^[:space:]]*//' | sort -u)
+
+          for IMG_NAME in $ARCH_IMAGES $MANIFESTS; do
             # Extract Docker image reference plus digest from local image
             URL=$(docker inspect --format='{{index .RepoDigests 0}}' "${IMG_NAME}")
 
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 1e1eb705..d54ca504 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -452,6 +452,7 @@ spec:
                     ignore:
                       description: IgnoreOptions can be used to ignore fields when
                         monitoring the bundle.
+                      nullable: true
                       properties:
                         conditions:
                           description: Conditions is a list of conditions to be ignored
@@ -821,6 +822,7 @@ spec:
                     ignore:
                       description: IgnoreOptions can be used to ignore fields when
                         monitoring the bundle.
+                      nullable: true
                       properties:
                         conditions:
                           description: Conditions is a list of conditions to be ignored
@@ -1741,6 +1743,7 @@ spec:
                 ignore:
                   description: IgnoreOptions can be used to ignore fields when monitoring
                     the bundle.
+                  nullable: true
                   properties:
                     conditions:
                       description: Conditions is a list of conditions to be ignored
@@ -2611,6 +2614,7 @@ spec:
                       ignore:
                         description: IgnoreOptions can be used to ignore fields when
                           monitoring the bundle.
+                        nullable: true
                         properties:
                           conditions:
                             description: Conditions is a list of conditions to be
@@ -7535,6 +7539,7 @@ spec:
                 ignore:
                   description: IgnoreOptions can be used to ignore fields when monitoring
                     the bundle.
+                  nullable: true
                   properties:
                     conditions:
                       description: Conditions is a list of conditions to be ignored
@@ -8422,6 +8427,7 @@ spec:
                       ignore:
                         description: IgnoreOptions can be used to ignore fields when
                           monitoring the bundle.
+                        nullable: true
                         properties:
                           conditions:
                             description: Conditions is a list of conditions to be
diff --git a/go.mod b/go.mod
index b10311ad..57420da2 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ replace (
 )
 
 require (
-	github.com/Masterminds/semver/v3 v3.3.1
+	github.com/Masterminds/semver/v3 v3.4.0
 	github.com/Masterminds/sprig/v3 v3.3.0
 	github.com/chartmuseum/helm-push v0.10.4
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
@@ -66,9 +66,9 @@ require (
 	sigs.k8s.io/cli-utils v0.37.2
 	sigs.k8s.io/controller-runtime v0.21.0
 	sigs.k8s.io/controller-tools v0.18.0
-	sigs.k8s.io/kustomize/api v0.19.0
-	sigs.k8s.io/kustomize/kyaml v0.19.0
-	sigs.k8s.io/yaml v1.4.0
+	sigs.k8s.io/kustomize/api v0.20.0
+	sigs.k8s.io/kustomize/kyaml v0.20.0
+	sigs.k8s.io/yaml v1.5.0
 )
 
 require (
@@ -89,6 +89,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
+	github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/chai2010/gettext-go v1.0.2 // indirect
@@ -136,7 +137,6 @@ require (
 	github.com/google/gnostic-models v0.6.9 // indirect
 	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
-	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
@@ -221,6 +221,8 @@ require (
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v3 v3.0.3 // indirect
 	golang.org/x/mod v0.25.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
diff --git a/go.sum b/go.sum
index 12dccf20..dd9acdf2 100644
--- a/go.sum
+++ b/go.sum
@@ -622,8 +622,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
-github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
 github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
 github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
@@ -661,6 +661,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/carapace-sh/carapace-shlex v1.0.1 h1:ww0JCgWpOVuqWG7k3724pJ18Lq8gh5pHQs9j3ojUs1c=
+github.com/carapace-sh/carapace-shlex v1.0.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -949,8 +951,6 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
-github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
-github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -1359,6 +1359,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -2170,15 +2174,16 @@ sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9x
 sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
-sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
-sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
-sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
-sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
+sigs.k8s.io/kustomize/api v0.20.0 h1:xPLqcobHI0bThyRUteO+nCV8G4d1Rlo5HafO57VRcas=
+sigs.k8s.io/kustomize/api v0.20.0/go.mod h1:F6CfaV27oevRCMJgehLqyX81dlUnRX/Fc13Uo7+OSo4=
+sigs.k8s.io/kustomize/kyaml v0.20.0 h1:tT8KMKi4R3hCJ1+9HDdek2VoXpkerP92ZfF6fDgGw14=
+sigs.k8s.io/kustomize/kyaml v0.20.0/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
 sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/internal/bundlereader/charturl.go b/internal/bundlereader/charturl.go
index c34e4385..6ecad766 100644
--- a/internal/bundlereader/charturl.go
+++ b/internal/bundlereader/charturl.go
@@ -17,7 +17,7 @@ import (
 // ChartVersion returns the version of the helm chart from a helm repo server, by
 // inspecting the repo's index.yaml
 func ChartVersion(location fleet.HelmOptions, auth Auth) (string, error) {
-	if hasOCIURL.MatchString(location.Chart) {
+	if hasOCIURL.MatchString(location.Repo) {
 		return location.Version, nil
 	}
 
@@ -43,9 +43,14 @@ func ChartVersion(location fleet.HelmOptions, auth Auth) (string, error) {
 
 // chartURL returns the URL to the helm chart from a helm repo server, by
 // inspecting the repo's index.yaml
-func chartURL(location fleet.HelmOptions, auth Auth) (string, error) {
-	if hasOCIURL.MatchString(location.Chart) {
-		return location.Chart, nil
+func chartURL(location fleet.HelmOptions, auth Auth, isHelmOps bool) (string, error) {
+	OCIField := location.Chart
+	if isHelmOps {
+		OCIField = location.Repo
+	}
+
+	if hasOCIURL.MatchString(OCIField) {
+		return OCIField, nil
 	}
 
 	if location.Repo == "" {
diff --git a/internal/bundlereader/helm.go b/internal/bundlereader/helm.go
index 7a0f86b0..3a278d38 100644
--- a/internal/bundlereader/helm.go
+++ b/internal/bundlereader/helm.go
@@ -32,7 +32,7 @@ func GetManifestFromHelmChart(ctx context.Context, c client.Client, bd *fleet.Bu
 	}
 	auth.InsecureSkipVerify = bd.Spec.HelmChartOptions.InsecureSkipTLSverify
 
-	chartURL, err := chartURL(*helm, auth)
+	chartURL, err := chartURL(*helm, auth, true)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/bundlereader/resources.go b/internal/bundlereader/resources.go
index 2a1762aa..7aac436d 100644
--- a/internal/bundlereader/resources.go
+++ b/internal/bundlereader/resources.go
@@ -202,7 +202,7 @@ func addRemoteCharts(directories []directory, base string, charts []*fleet.HelmO
 				auth = Auth{}
 			}
 
-			chartURL, err := chartURL(*chart, auth)
+			chartURL, err := chartURL(*chart, auth, false)
 			if err != nil {
 				return nil, fmt.Errorf("failed to resolve URL of %s: %w", downloadChartError(*chart), err)
 			}
diff --git a/internal/bundlereader/style.go b/internal/bundlereader/style.go
index 834ef09b..b8a1b76f 100644
--- a/internal/bundlereader/style.go
+++ b/internal/bundlereader/style.go
@@ -17,10 +17,16 @@ func joinAndClean(path, file string) string {
 }
 
 func chartPath(options fleet.BundleDeploymentOptions) (string, string) {
-	if options.Helm == nil || options.Helm.Chart == "" {
+	if options.Helm == nil {
 		return chartYAML, ""
 	}
-	return joinAndClean(options.Helm.Chart, chartYAML), checksum(options.Helm) + "/"
+
+	path := options.Helm.Chart
+	if len(path) == 0 {
+		path = options.Helm.Repo
+	}
+
+	return joinAndClean(path, chartYAML), checksum(options.Helm) + "/"
 }
 
 func kustomizePath(options fleet.BundleDeploymentOptions) string {
diff --git a/internal/cmd/controller/helmops/reconciler/helmop_controller.go b/internal/cmd/controller/helmops/reconciler/helmop_controller.go
index fc1f8787..55c711b2 100644
--- a/internal/cmd/controller/helmops/reconciler/helmop_controller.go
+++ b/internal/cmd/controller/helmops/reconciler/helmop_controller.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 	"time"
 
 	"k8s.io/apimachinery/pkg/api/equality"
@@ -123,16 +124,16 @@ func (r *HelmOpReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{Requeue: true}, nil
 	}
 
+	if err := validate(ctx, *helmop); err != nil {
+		return ctrl.Result{}, updateErrorStatusHelm(ctx, r.Client, req.NamespacedName, helmop, err)
+	}
+
 	// Reconciling
 	logger = logger.WithValues("generation", helmop.Generation, "chart", helmop.Spec.Helm.Chart)
 	ctx = log.IntoContext(ctx, logger)
 
 	logger.V(1).Info("Reconciling HelmOp")
 
-	if helmop.Spec.Helm.Chart == "" {
-		return ctrl.Result{}, nil
-	}
-
 	if _, err := r.createUpdateBundle(ctx, helmop); err != nil {
 		return ctrl.Result{}, updateErrorStatusHelm(ctx, r.Client, req.NamespacedName, helmop, err)
 	}
@@ -529,6 +530,46 @@ func jobKey(h fleet.HelmOp) *quartz.JobKey {
 	return quartz.NewJobKey(string(h.UID))
 }
 
+// validate checks combinations of Chart, Repo and Version fields in h's Helm options.
+// It returns an error if those options are nil, or if they don't fall under any of these categories,
+// as per https://helm.sh/docs/helm/helm_install/ :
+// * tarball URL in Chart, empty Repo, empty Version
+// * OCI reference in the Repo field, empty Chart, optional Version
+// * non-empty Repo URL, non-empty Chart name, optional Version
+func validate(ctx context.Context, h fleet.HelmOp) error {
+	if h.Spec.Helm == nil {
+		return fmt.Errorf("helm options are empty in the HelmOp's spec")
+	}
+
+	fail := func(msg string) error {
+		return fmt.Errorf("helm options invalid: %s", msg)
+	}
+
+	if strings.HasSuffix(strings.ToLower(h.Spec.Helm.Chart), ".tgz") {
+		if len(h.Spec.Helm.Repo) > 0 {
+			return fail("tarball chart with a non-empty repo field")
+		}
+
+		if len(h.Spec.Helm.Version) > 0 {
+			return fail("tarball chart with a non-empty version field")
+		}
+	} else if strings.HasPrefix(strings.ToLower(h.Spec.Helm.Repo), "oci://") {
+		if len(h.Spec.Helm.Chart) > 0 {
+			return fail("OCI repository with a non-empty chart field")
+		}
+	} else { // Expecting full reference: chart + repo + optional version
+		if len(h.Spec.Helm.Chart) == 0 {
+			return fail("non-OCI repository with an empty chart field")
+		}
+
+		if len(h.Spec.Helm.Repo) == 0 {
+			return fail("non-tarball chart with an empty repo field")
+		}
+	}
+
+	return nil
+}
+
 // helmOpTrigger is a custom trigger, implementing the quartz.Trigger interface. This trigger is
 // used to schedule jobs to be run both:
 // * periodically, after the first polling interval, as would happen with Quartz's `simpleTrigger`
diff --git a/internal/cmd/controller/helmops/reconciler/helmop_status.go b/internal/cmd/controller/helmops/reconciler/helmop_status.go
index 76f2b7a0..839bf972 100644
--- a/internal/cmd/controller/helmops/reconciler/helmop_status.go
+++ b/internal/cmd/controller/helmops/reconciler/helmop_status.go
@@ -77,10 +77,6 @@ func (r *HelmOpStatusReconciler) Reconcile(ctx context.Context, req ctrl.Request
 		return ctrl.Result{}, nil
 	}
 
-	if helmop.Spec.Helm.Chart == "" {
-		return ctrl.Result{}, nil
-	}
-
 	logger = logger.WithValues("generation", helmop.Generation, "chart", helmop.Spec.Helm.Chart).WithValues("conditions", helmop.Status.Conditions)
 	ctx = log.IntoContext(ctx, logger)
 
diff --git a/internal/cmd/controller/reconciler/bundle_controller.go b/internal/cmd/controller/reconciler/bundle_controller.go
index 57f868cb..04c4c07d 100644
--- a/internal/cmd/controller/reconciler/bundle_controller.go
+++ b/internal/cmd/controller/reconciler/bundle_controller.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/Masterminds/semver/v3"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/kv"
+	fleetutil "github.com/rancher/fleet/internal/cmd/controller/errorutil"
 	"github.com/rancher/fleet/internal/cmd/controller/finalize"
 	"github.com/rancher/fleet/internal/cmd/controller/summary"
 	"github.com/rancher/fleet/internal/cmd/controller/target"
@@ -23,10 +24,11 @@ import (
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	fleetevent "github.com/rancher/fleet/pkg/event"
 	"github.com/rancher/fleet/pkg/sharding"
-	"github.com/rancher/wrangler/v3/pkg/genericcondition"
+	"github.com/rancher/wrangler/v3/pkg/condition"
 
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/equality"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -228,18 +230,13 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 	// Skip bundle deployment creation if the bundle is a HelmOps bundle and the configured Helm version is still a
 	// version constraint. That constraint should be resolved into a strict version by the HelmOps reconciler before bundle
 	// deployments can be created.
-	if contentsInHelmChart && bundle.Spec.Helm != nil {
-		version := bundle.Spec.Helm.Version
-
-		if _, err := semver.StrictNewVersion(version); err != nil {
-			bundle.Status.Conditions = []genericcondition.GenericCondition{
-				{
-					Type:           string(fleet.Ready),
-					Status:         corev1.ConditionFalse,
-					Message:        fmt.Sprintf("Chart version cannot be deployed; check HelmOp status for more details: %v", err),
-					LastUpdateTime: metav1.Now().UTC().Format(time.RFC3339),
-				},
-			}
+	if contentsInHelmChart && bundle.Spec.Helm != nil && len(bundle.Spec.Helm.Version) > 0 {
+		if _, err := semver.StrictNewVersion(bundle.Spec.Helm.Version); err != nil {
+			setReadyCondition(
+				&bundle.Status,
+				fmt.Errorf("chart version cannot be deployed; check HelmOp status for more details: %v", err),
+			)
+
 			err := r.updateStatus(ctx, bundleOrig, bundle)
 			return ctrl.Result{}, err
 		}
@@ -270,14 +267,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 	if err != nil {
 		// When targeting fails, we don't want to continue and we make the error message visible in
 		// the UI. For that we use a status condition of type Ready.
-		bundle.Status.Conditions = []genericcondition.GenericCondition{
-			{
-				Type:           string(fleet.Ready),
-				Status:         corev1.ConditionFalse,
-				Message:        "Targeting error: " + err.Error(),
-				LastUpdateTime: metav1.Now().UTC().Format(time.RFC3339),
-			},
-		}
+		setReadyCondition(&bundle.Status, fmt.Errorf("targeting error: %v", err))
 
 		err := r.updateStatus(ctx, bundleOrig, bundle)
 		return ctrl.Result{}, err
@@ -369,25 +359,25 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 			bundle.Spec.HelmOpOptions != nil,
 			manifestID)
 		if err != nil {
-			return ctrl.Result{}, err
+			return ctrl.Result{}, fmt.Errorf("failed to create bundle deployment: %w", err)
 		}
 		bundleDeploymentUIDs.Insert(bd.UID)
 
 		if bd.Spec.ValuesHash != "" {
 			if err := r.createOptionsSecret(ctx, bd, options, stagedOptions); err != nil {
-				return ctrl.Result{}, err
+				return ctrl.Result{}, fmt.Errorf("failed to create options secret: %w", err)
 			}
 		} else {
 			// No values to store, delete the secret if it exists
 			if err := r.Delete(ctx, &corev1.Secret{
 				ObjectMeta: metav1.ObjectMeta{Name: bd.Name, Namespace: bd.Namespace},
 			}); err != nil && !apierrors.IsNotFound(err) {
-				return ctrl.Result{}, err
+				return ctrl.Result{}, fmt.Errorf("failed to delete options secret: %w", err)
 			}
 		}
 
 		if err := r.handleContentAccessSecrets(ctx, bundle, bd); err != nil {
-			return ctrl.Result{}, err
+			return ctrl.Result{}, fmt.Errorf("failed to clone secrets downstream: %w", err)
 		}
 	}
 
@@ -742,3 +732,13 @@ func batchDeleteBundleDeployments(ctx context.Context, c client.Client, list []f
 
 	return errors.Join(errs...)
 }
+
+// setCondition sets the condition and updates the timestamp, if the condition changed
+func setReadyCondition(status *fleet.BundleStatus, err error) {
+	cond := condition.Cond(fleet.Ready)
+	origStatus := status.DeepCopy()
+	cond.SetError(status, "", fleetutil.IgnoreConflict(err))
+	if !equality.Semantic.DeepEqual(origStatus, status) {
+		cond.LastUpdated(status, time.Now().UTC().Format(time.RFC3339))
+	}
+}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
index 17f069f6..bd85ba29 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
@@ -97,8 +97,8 @@ type BundleDeploymentOptions struct {
 	DeleteNamespace bool `json:"deleteNamespace,omitempty"`
 
 	//IgnoreOptions can be used to ignore fields when monitoring the bundle.
-	// +optional
-	IgnoreOptions `json:"ignore,omitempty"`
+	// +nullable
+	IgnoreOptions *IgnoreOptions `json:"ignore,omitempty"`
 
 	// CorrectDrift specifies how drift correction should work.
 	CorrectDrift *CorrectDrift `json:"correctDrift,omitempty"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
index f3433732..fab85e60 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -174,7 +174,11 @@ func (in *BundleDeploymentOptions) DeepCopyInto(out *BundleDeploymentOptions) {
 		*out = new(DiffOptions)
 		(*in).DeepCopyInto(*out)
 	}
-	in.IgnoreOptions.DeepCopyInto(&out.IgnoreOptions)
+	if in.IgnoreOptions != nil {
+		in, out := &in.IgnoreOptions, &out.IgnoreOptions
+		*out = new(IgnoreOptions)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.CorrectDrift != nil {
 		in, out := &in.CorrectDrift, &out.CorrectDrift
 		*out = new(CorrectDrift)



Test patch:
diff --git a/e2e/single-cluster/helm_dependencies_test.go b/e2e/single-cluster/helm_dependencies_test.go
index 7db489a3..61833fbc 100644
--- a/e2e/single-cluster/helm_dependencies_test.go
+++ b/e2e/single-cluster/helm_dependencies_test.go
@@ -1,7 +1,6 @@
 package singlecluster_test
 
 import (
-	"fmt"
 	"math/rand"
 	"os"
 	"path"
@@ -10,7 +9,6 @@ import (
 
 	"github.com/rancher/fleet/e2e/testenv"
 	"github.com/rancher/fleet/e2e/testenv/githelper"
-	"github.com/rancher/fleet/e2e/testenv/infra/cmd"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -18,14 +16,6 @@ import (
 	cp "github.com/otiai10/copy"
 )
 
-func getChartMuseumExternalAddr() string {
-	username := os.Getenv("GIT_HTTP_USER")
-	passwd := os.Getenv("GIT_HTTP_PASSWORD")
-	Expect(username).ToNot(Equal(""))
-	Expect(passwd).ToNot(Equal(""))
-	return fmt.Sprintf("https://%s:%s@chartmuseum-service.%s.svc.cluster.local:8081", username, passwd, cmd.InfraNamespace)
-}
-
 func setupChartDepsInTmpDir(chartDir string, tmpDir string, namespace string, disableDependencyUpdate bool) {
 	err := cp.Copy(chartDir, tmpDir)
 	Expect(err).ToNot(HaveOccurred())
diff --git a/e2e/single-cluster/helmop_test.go b/e2e/single-cluster/helmop_test.go
index 0b517d21..5f7b1301 100644
--- a/e2e/single-cluster/helmop_test.go
+++ b/e2e/single-cluster/helmop_test.go
@@ -2,6 +2,7 @@ package singlecluster_test
 
 import (
 	"crypto/tls"
+	"encoding/json"
 	"fmt"
 	"math/rand"
 	"net/http"
@@ -13,7 +14,7 @@ import (
 	"github.com/rancher/fleet/e2e/testenv"
 	"github.com/rancher/fleet/e2e/testenv/infra/cmd"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
-	"github.com/rancher/fleet/e2e/testenv/zothelper"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
 	"github.com/chartmuseum/helm-push/pkg/chartmuseum"
 	. "github.com/onsi/ginkgo/v2"
@@ -177,11 +178,13 @@ var _ = Describe("HelmOp resource tests with polling", Label("infra-setup", "hel
 	})
 })
 
-var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup", "oci-registry"), func() {
+var _ = Describe("HelmOp resource tests with OCI registry", Label("infra-setup", "oci-registry"), func() {
 	var (
 		namespace string
 		name      string
+		repo      string
 		insecure  bool
+		ociRef    = getZotInternalRef()
 		k         kubectl.Command
 	)
 
@@ -205,9 +208,6 @@ var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup",
 		}
 		Expect(err).ToNot(HaveOccurred(), out)
 
-		ociRef, err := zothelper.GetOCIReference(k)
-		Expect(err).ToNot(HaveOccurred(), ociRef)
-
 		err = testenv.ApplyTemplate(k, testenv.AssetPath("helmop/helmop.yaml"), struct {
 			Name                  string
 			Namespace             string
@@ -220,8 +220,8 @@ var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup",
 		}{
 			name,
 			namespace,
+			repo,
 			"",
-			fmt.Sprintf("%s/sleeper-chart", ociRef),
 			0,
 			helmOpsSecretName,
 			insecure,
@@ -243,16 +243,18 @@ var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup",
 				namespace = "helmop-ns"
 				name = "basic-oci"
 				insecure = true
+
+				repo = fmt.Sprintf("%s/sleeper-chart", ociRef)
 			})
 			It("deploys the chart", func() {
-				Eventually(func() bool {
+				Eventually(func(g Gomega) {
 					outPods, _ := k.Namespace(namespace).Get("pods")
-					return strings.Contains(outPods, "sleeper-")
-				}).Should(BeTrue())
-				Eventually(func() bool {
+					g.Expect(outPods).To(ContainSubstring("sleeper-"))
+				}).Should(Succeed())
+				Eventually(func(g Gomega) {
 					outDeployments, _ := k.Namespace(namespace).Get("deployments")
-					return strings.Contains(outDeployments, "sleeper")
-				}).Should(BeTrue())
+					g.Expect(outDeployments).To(ContainSubstring("sleeper"))
+				}).Should(Succeed())
 			})
 		})
 		Context("containing a valid helmop description pointing to an oci registry and not TLS", func() {
@@ -260,6 +262,8 @@ var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup",
 				namespace = "helmop-ns2"
 				name = "basic-oci-no-tls"
 				insecure = false
+
+				repo = fmt.Sprintf("%s/sleeper-chart", ociRef)
 			})
 			It("does not deploy the chart because of TLS", func() {
 				Consistently(func() string {
@@ -269,6 +273,133 @@ var _ = Describe("HelmOp resource tests with oci registry", Label("infra-setup",
 			})
 		})
 	})
+
+	When("applying a helmop resource which cannot be deployed", func() {
+		Context("containing a helmop description pointing to an OCI registry using the wrong field", func() {
+			BeforeEach(func() {
+				namespace = "helmop-ns"
+				name = "basic-oci-invalid"
+				insecure = true
+
+				repo = fmt.Sprintf("%s/sleeper-chart-will-not-be-found", ociRef)
+			})
+			It("fails visibly", func() {
+				By("not deploying the chart")
+				Consistently(func(g Gomega) {
+					outPods, _ := k.Namespace(namespace).Get("pods")
+					g.Expect(outPods).NotTo(ContainSubstring("sleeper-"))
+
+					outDeployments, _ := k.Namespace(namespace).Get("deployments")
+					g.Expect(outDeployments).NotTo(ContainSubstring("sleeper"))
+				}, 5*time.Second, time.Second).Should(Succeed())
+
+				By("displaying the reason for the failure in the HelmOps' status")
+				Eventually(func(g Gomega) {
+					st, err := k.Get("helmop", name, "-o=jsonpath={.status}")
+					g.Expect(err).ToNot(HaveOccurred())
+
+					var status fleet.StatusBase
+					err = json.Unmarshal([]byte(st), &status)
+					g.Expect(err).ToNot(HaveOccurred())
+
+					g.Expect(status.Conditions).ToNot(BeEmpty())
+
+					var foundReady, foundAccepted bool
+					for _, cond := range status.Conditions {
+						if cond.Type == "Ready" {
+							foundReady = true
+
+							g.Expect(string(cond.Status)).To(Equal("False"))
+							g.Expect(cond.Message).To(ContainSubstring("ErrApplied"))
+							g.Expect(cond.Message).To(ContainSubstring("not found"))
+						}
+
+						if cond.Type == "Accepted" {
+							foundAccepted = true
+							g.Expect(string(cond.Status)).To(Equal("True"))
+						}
+					}
+					g.Expect(foundAccepted).To(BeTrue())
+					g.Expect(foundReady).To(BeTrue())
+				}).Should(Succeed())
+			})
+		})
+	})
+})
+
+var _ = Describe("HelmOp resource tests with tarball source", Label("infra-setup", "helm-registry"), func() {
+	var (
+		namespace string
+		name      string
+		insecure  = true
+		k         kubectl.Command
+		version   string
+	)
+
+	BeforeEach(func() {
+		k = env.Kubectl.Namespace(env.Namespace)
+	})
+
+	JustBeforeEach(func() {
+		namespace = testenv.NewNamespaceName(
+			name,
+			rand.New(rand.NewSource(time.Now().UnixNano())),
+		)
+
+		out, err := k.Create(
+			"secret", "generic", helmOpsSecretName,
+			"--from-literal=username="+os.Getenv("CI_OCI_USERNAME"),
+			"--from-literal=password="+os.Getenv("CI_OCI_PASSWORD"),
+		)
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		err = testenv.ApplyTemplate(k, testenv.AssetPath("helmop/helmop.yaml"), struct {
+			Name                  string
+			Namespace             string
+			Repo                  string
+			Chart                 string
+			PollingInterval       time.Duration
+			HelmSecretName        string
+			InsecureSkipTLSVerify bool
+			Version               string
+		}{
+			name,
+			namespace,
+			"",
+			fmt.Sprintf("%s/charts/sleeper-chart-0.1.0.tgz", getChartMuseumExternalAddr()),
+			0,
+			helmOpsSecretName,
+			insecure,
+			version,
+		})
+		Expect(err).ToNot(HaveOccurred(), out)
+	})
+
+	AfterEach(func() {
+		out, err := k.Delete("helmop", name)
+		Expect(err).ToNot(HaveOccurred(), out)
+		out, err = k.Delete("secret", helmOpsSecretName)
+		Expect(err).ToNot(HaveOccurred(), out)
+	})
+
+	// Other version combinations are tested in HelmOps controller unit test
+	When("applying a helmop resource without a version", func() {
+		BeforeEach(func() {
+			namespace = "helmop-tarball-ns-no-version"
+			name = "basic-helmop"
+			version = ""
+		})
+		It("deploys the chart", func() {
+			Eventually(func(g Gomega) {
+				outPods, _ := k.Namespace(namespace).Get("pods")
+				g.Expect(outPods).To(ContainSubstring("sleeper-"))
+			}).Should(Succeed())
+			Eventually(func(g Gomega) {
+				outDeployments, _ := k.Namespace(namespace).Get("deployments")
+				g.Expect(outDeployments).To(ContainSubstring("sleeper"))
+			}).Should(Succeed())
+		})
+	})
 })
 
 // getExternalHelmAddr retrieves the external URL where our local Helm registry can be reached.
diff --git a/e2e/single-cluster/status_test.go b/e2e/single-cluster/status_test.go
index ef4d66a2..97547515 100644
--- a/e2e/single-cluster/status_test.go
+++ b/e2e/single-cluster/status_test.go
@@ -206,7 +206,7 @@ var _ = Describe("Checks that template errors are shown in bundles and gitrepos"
 		for _, condition := range conditions {
 			if condition.Type == string(fleet.Ready) {
 				g.Expect(condition.Status).To(Equal(corev1.ConditionFalse))
-				g.Expect(condition.Message).To(ContainSubstring("Targeting error"))
+				g.Expect(condition.Message).To(ContainSubstring("targeting error"))
 				g.Expect(condition.Message).To(
 					ContainSubstring(
 						"<.ClusterLabels.foo>: map has no entry for key \"foo\""))
diff --git a/e2e/single-cluster/suite_test.go b/e2e/single-cluster/suite_test.go
index 3a0600aa..be77362b 100644
--- a/e2e/single-cluster/suite_test.go
+++ b/e2e/single-cluster/suite_test.go
@@ -2,6 +2,8 @@
 package singlecluster_test
 
 import (
+	"fmt"
+	"os"
 	"testing"
 	"time"
 
@@ -14,6 +16,7 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 
 	"github.com/rancher/fleet/e2e/testenv"
+	"github.com/rancher/fleet/e2e/testenv/infra/cmd"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -67,3 +70,15 @@ func getClientForContext(contextName string) client.Client {
 
 	return c
 }
+
+func getChartMuseumExternalAddr() string {
+	username := os.Getenv("GIT_HTTP_USER")
+	passwd := os.Getenv("GIT_HTTP_PASSWORD")
+	Expect(username).ToNot(Equal(""))
+	Expect(passwd).ToNot(Equal(""))
+	return fmt.Sprintf("https://%s:%s@chartmuseum-service.%s.svc.cluster.local:8081", username, passwd, cmd.InfraNamespace)
+}
+
+func getZotInternalRef() string {
+	return fmt.Sprintf("oci://zot-service.%s.svc.cluster.local:8082", cmd.InfraNamespace)
+}
diff --git a/integrationtests/helmops/controller/status_test.go b/integrationtests/helmops/controller/status_test.go
index d8e4b436..ed03a7c3 100644
--- a/integrationtests/helmops/controller/status_test.go
+++ b/integrationtests/helmops/controller/status_test.go
@@ -58,7 +58,7 @@ var _ = Describe("HelmOp Status Fields", func() {
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart: "test",
+								Chart: "http://foo.bar/baz/test.tgz",
 							},
 						},
 					},
diff --git a/internal/cmd/agent/deployer/monitor/conditions_test.go b/internal/cmd/agent/deployer/monitor/conditions_test.go
index 71bc31bb..093eedf9 100644
--- a/internal/cmd/agent/deployer/monitor/conditions_test.go
+++ b/internal/cmd/agent/deployer/monitor/conditions_test.go
@@ -27,37 +27,43 @@ func TestExcludeIgnoredConditions(t *testing.T) {
 	}
 	tests := map[string]struct {
 		obj           *unstructured.Unstructured
-		ignoreOptions fleet.IgnoreOptions
+		ignoreOptions *fleet.IgnoreOptions
 		expectedObj   *unstructured.Unstructured
 		expectedErr   error
 	}{
-		"nothing is changed without IgnoreOptions": {
+		"nothing is changed with empty IgnoreOptions": {
 			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
-			ignoreOptions: fleet.IgnoreOptions{},
+			ignoreOptions: &fleet.IgnoreOptions{},
+			expectedObj:   &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
+			expectedErr:   nil,
+		},
+		"nothing is changed with nil IgnoreOptions": {
+			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
+			ignoreOptions: nil,
 			expectedObj:   &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
 			expectedErr:   nil,
 		},
 		"nothing is changed when IgnoreOptions don't match any condition": {
 			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
-			ignoreOptions: fleet.IgnoreOptions{Conditions: []map[string]string{{"Not": "Found"}}},
+			ignoreOptions: &fleet.IgnoreOptions{Conditions: []map[string]string{{"Not": "Found"}}},
 			expectedObj:   &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
 			expectedErr:   nil,
 		},
 		"'Type: Ready' condition is excluded when IgnoreOptions contains 'Type: Ready' condition": {
 			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
-			ignoreOptions: fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready"}}},
+			ignoreOptions: &fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready"}}},
 			expectedObj:   &unstructured.Unstructured{Object: uPodInitialized},
 			expectedErr:   nil,
 		},
 		"'Type: Ready' condition is excluded when IgnoreOptions contains 'Type: Ready, status: False' condition": {
 			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
-			ignoreOptions: fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready", "status": "False"}}},
+			ignoreOptions: &fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready", "status": "False"}}},
 			expectedObj:   &unstructured.Unstructured{Object: uPodInitialized},
 			expectedErr:   nil,
 		},
 		"nothing is changed when IgnoreOptions contains 'type: Ready, status: True' condition": {
 			obj:           &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
-			ignoreOptions: fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready", "status": "True"}}},
+			ignoreOptions: &fleet.IgnoreOptions{Conditions: []map[string]string{{"type": "Ready", "status": "True"}}},
 			expectedObj:   &unstructured.Unstructured{Object: uPodInitializedAndNotReady},
 			expectedErr:   nil,
 		},
diff --git a/internal/cmd/agent/deployer/monitor/updatestatus.go b/internal/cmd/agent/deployer/monitor/updatestatus.go
index 018f83f6..283981d8 100644
--- a/internal/cmd/agent/deployer/monitor/updatestatus.go
+++ b/internal/cmd/agent/deployer/monitor/updatestatus.go
@@ -312,7 +312,7 @@ func calculateDesiredReady(liveResourceKeys map[fleet.ResourceKey]struct{}, modi
 	return desired
 }
 
-func nonReady(ctx context.Context, plan desiredset.Plan, ignoreOptions fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
+func nonReady(ctx context.Context, plan desiredset.Plan, ignoreOptions *fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
 	logger := log.FromContext(ctx)
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
@@ -322,7 +322,7 @@ func nonReady(ctx context.Context, plan desiredset.Plan, ignoreOptions fleet.Ign
 
 	for _, obj := range plan.Objects {
 		if u, ok := obj.(*unstructured.Unstructured); ok {
-			if ignoreOptions.Conditions != nil {
+			if ignoreOptions != nil && ignoreOptions.Conditions != nil {
 				if err := excludeIgnoredConditions(u, ignoreOptions); err != nil {
 					logger.Error(err, "failed to ignore conditions")
 				}
@@ -438,7 +438,11 @@ func isResourceInPreviousRelease(key objectset.ObjectKey, kind string, objsPrevi
 }
 
 // excludeIgnoredConditions removes the conditions that are included in ignoreOptions from the object passed as a parameter
-func excludeIgnoredConditions(obj *unstructured.Unstructured, ignoreOptions fleet.IgnoreOptions) error {
+func excludeIgnoredConditions(obj *unstructured.Unstructured, ignoreOptions *fleet.IgnoreOptions) error {
+	if ignoreOptions == nil {
+		return nil
+	}
+
 	conditions, _, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
 	if err != nil {
 		return err
diff --git a/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go b/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
index c8e1c80c..8c38ad12 100644
--- a/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
+++ b/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
@@ -33,6 +33,51 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
 
+const (
+	helmRepoIndex = `apiVersion: v1
+entries:
+  alpine:
+    - created: 2016-10-06T16:23:20.499814565-06:00
+      description: Deploy a basic Alpine Linux pod
+      digest: 99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd
+      home: https://helm.sh/helm
+      name: alpine
+      sources:
+      - https://github.com/helm/helm
+      urls:
+      - https://technosophos.github.io/tscharts/alpine-0.2.0.tgz
+      version: 0.2.0
+    - created: 2016-10-06T16:23:20.499543808-06:00
+      description: Deploy a basic Alpine Linux pod
+      digest: 515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727
+      home: https://helm.sh/helm
+      name: alpine
+      sources:
+      - https://github.com/helm/helm
+      urls:
+      - https://technosophos.github.io/tscharts/alpine-0.1.0.tgz
+      version: 0.1.0
+  nginx:
+    - created: 2016-10-06T16:23:20.499543808-06:00
+      description: Create a basic nginx HTTP server
+      digest: aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff
+      home: https://helm.sh/helm
+      name: nginx
+      sources:
+      - https://github.com/helm/charts
+      urls:
+      - https://technosophos.github.io/tscharts/nginx-0.1.0.tgz
+      version: 0.1.0
+generated: 2016-10-06T16:23:20.499029981-06:00`
+)
+
+func createHelmServer() *httptest.Server {
+	return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprint(w, helmRepoIndex)
+	}))
+}
+
 func getCondition(helmop *fleet.HelmOp, condType string) (genericcondition.GenericCondition, bool) {
 	for _, cond := range helmop.Status.Conditions {
 		if cond.Type == condType {
@@ -86,6 +131,223 @@ func TestReconcile_ReturnsAndRequeuesAfterAddingFinalizer(t *testing.T) {
 	}
 }
 
+func TestReconcile_Validate(t *testing.T) {
+	cases := []struct {
+		name   string
+		helmop fleet.HelmOp
+		err    string
+	}{
+		{
+			name: "error if Helm spec is empty",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{},
+			},
+			err: "helm options are empty",
+		},
+		{
+			name: "error if tarball chart with non-empty repo",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart: "https://foo.bar/baz.tgz",
+								Repo:  "non-empty",
+							},
+						},
+					},
+				},
+			},
+			err: "tarball chart with a non-empty repo field",
+		},
+		{
+			name: "error if tarball chart with non-empty nor wildcard version",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart:   "https://foo.bar/baz.tgz",
+								Version: "1.2.3",
+							},
+						},
+					},
+				},
+			},
+			err: "tarball chart with a non-empty version field",
+		},
+		{
+			name: "error if tarball chart with 'latest' version",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart:   "https://foo.bar/baz.tgz",
+								Version: "latest",
+							},
+						},
+					},
+				},
+			},
+			err: "tarball chart with a non-empty version field",
+		},
+		{
+			name: "error if tarball chart with wildcard version",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart:   "https://foo.bar/baz.tgz",
+								Version: "*",
+							},
+						},
+					},
+				},
+			},
+			err: "tarball chart with a non-empty version field",
+		},
+		{
+			name: "error if OCI repo with non-empty chart",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart:   "hello",
+								Repo:    "oci://foo/bar/baz",
+								Version: "*",
+							},
+						},
+					},
+				},
+			},
+			err: "OCI repository with a non-empty chart field",
+		},
+		{
+			name: "error if non-OCI repo with empty chart",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Repo: "https://foo/bar/baz",
+							},
+						},
+					},
+				},
+			},
+			err: "non-OCI repository with an empty chart field",
+		},
+		{
+			name: "error if non-tarball chart with empty repo",
+			helmop: fleet.HelmOp{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "helmop",
+					Namespace: "default",
+				},
+				Spec: fleet.HelmOpSpec{
+					BundleSpec: fleet.BundleSpec{
+						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
+							Helm: &fleet.HelmOptions{
+								Chart: "foo",
+							},
+						},
+					},
+				},
+			},
+			err: "non-tarball chart with an empty repo field",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			defer mockCtrl.Finish()
+			scheme := runtime.NewScheme()
+			utilruntime.Must(batchv1.AddToScheme(scheme))
+
+			namespacedName := types.NamespacedName{Name: c.helmop.Name, Namespace: c.helmop.Namespace}
+			client := mocks.NewMockClient(mockCtrl)
+			client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+				func(ctx context.Context, req types.NamespacedName, fh *fleet.HelmOp, opts ...interface{}) error {
+					fh.Name = c.helmop.Name
+					fh.Namespace = c.helmop.Namespace
+					fh.Spec = c.helmop.Spec
+
+					controllerutil.AddFinalizer(fh, finalize.HelmOpFinalizer)
+					return nil
+				},
+			)
+
+			client.EXPECT().Get(gomock.Any(), namespacedName, matchesBundle(c.helmop.Name, c.helmop.Namespace), gomock.Any()).
+				DoAndReturn(
+					func(ctx context.Context, req types.NamespacedName, bundle *fleet.Bundle, opts ...interface{}) error {
+						bundle.ObjectMeta = metav1.ObjectMeta{
+							Name:      c.helmop.Name,
+							Namespace: c.helmop.Namespace,
+						}
+						bundle.Spec.HelmOpOptions = &fleet.BundleHelmOptions{
+							SecretName: "foo", //prevent collision errors; the value does not matter.
+						}
+
+						return nil
+					},
+				).AnyTimes()
+
+			// expected from addFinalizer
+			client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+			client.EXPECT().Update(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+
+			statusClient := mocks.NewMockSubResourceWriter(mockCtrl)
+			client.EXPECT().Status().Return(statusClient)
+			statusClient.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
+
+			r := HelmOpReconciler{
+				Client: client,
+				Scheme: scheme,
+			}
+
+			ctx := context.TODO()
+
+			_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName})
+			if err == nil || !strings.Contains(err.Error(), c.err) {
+				t.Errorf("unexpected error:\r\n\twanted\r\n\t\t%q in error message,\r\n\tgot \r\n\t\t%v", c.err, err)
+			}
+
+		})
+	}
+}
+
 func TestReconcile_ErrorCreatingBundleIsShownInStatus(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
@@ -104,8 +366,10 @@ func TestReconcile_ErrorCreatingBundleIsShownInStatus(t *testing.T) {
 			fh.Name = helmop.Name
 			fh.Namespace = helmop.Namespace
 			fh.Spec.Helm = &fleet.HelmOptions{
-				Chart: "chart",
+				Chart: "chart.tgz",
 			}
+			fh.Status = fleet.HelmOpStatus{}
+
 			controllerutil.AddFinalizer(fh, finalize.HelmOpFinalizer)
 			return nil
 		},
@@ -177,8 +441,10 @@ func TestReconcile_ErrorCreatingBundleIfBundleWithSameNameExists(t *testing.T) {
 			fh.Name = helmop.Name
 			fh.Namespace = helmop.Namespace
 			fh.Spec.Helm = &fleet.HelmOptions{
-				Chart: "chart",
+				Chart: "chart.tgz",
 			}
+			fh.Status = fleet.HelmOpStatus{}
+
 			controllerutil.AddFinalizer(fh, finalize.HelmOpFinalizer)
 			return nil
 		},
@@ -241,6 +507,9 @@ func TestReconcile_ErrorCreatingBundleIfBundleWithSameNameExists(t *testing.T) {
 }
 
 func TestReconcile_CreatesBundleAndUpdatesStatus(t *testing.T) {
+	svr1 := createHelmServer()
+	defer svr1.Close()
+
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
 	scheme := runtime.NewScheme()
@@ -257,29 +526,35 @@ func TestReconcile_CreatesBundleAndUpdatesStatus(t *testing.T) {
 		func(ctx context.Context, req types.NamespacedName, fh *fleet.HelmOp, opts ...interface{}) error {
 			fh.Name = helmop.Name
 			fh.Namespace = helmop.Namespace
+			fh.Spec.InsecureSkipTLSverify = true
 			fh.Spec.Helm = &fleet.HelmOptions{
-				Chart:   "chart",
-				Version: "1.1.x",
+				Repo:    svr1.URL,
+				Chart:   "alpine",
+				Version: "0.x.x",
 			}
+			fh.Status = fleet.HelmOpStatus{}
+
 			controllerutil.AddFinalizer(fh, finalize.HelmOpFinalizer)
 			return nil
 		},
 	)
 
-	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).DoAndReturn(
-		func(ctx context.Context, req types.NamespacedName, bundle *fleet.Bundle, opts ...interface{}) error {
-			return k8serrors.NewNotFound(schema.GroupResource{}, "Not found")
-		},
-	)
+	client.EXPECT().Get(gomock.Any(), namespacedName, gomock.Any(), gomock.Any()).Times(2).
+		DoAndReturn(
+			func(ctx context.Context, req types.NamespacedName, bundle *fleet.Bundle, opts ...interface{}) error {
+				return k8serrors.NewNotFound(schema.GroupResource{}, "Not found")
+			},
+		)
 
-	client.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
-		func(ctx context.Context, bundle *fleet.Bundle, opts ...interface{}) error {
-			return nil
-		},
-	)
+	client.EXPECT().Create(gomock.Any(), matchesBundle(helmop.Name, helmop.Namespace), gomock.Any()).
+		DoAndReturn(
+			func(ctx context.Context, bundle *fleet.Bundle, opts ...interface{}) error {
+				return nil
+			},
+		)
 
-	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
-		func(ctx context.Context, req types.NamespacedName, bundle *fleet.HelmOp, opts ...interface{}) error {
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), &fleet.HelmOp{}, gomock.Any()).Times(1).DoAndReturn(
+		func(ctx context.Context, req types.NamespacedName, _ *fleet.HelmOp, opts ...interface{}) error {
 			return nil
 		},
 	)
@@ -289,8 +564,8 @@ func TestReconcile_CreatesBundleAndUpdatesStatus(t *testing.T) {
 	statusClient.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(
 		func(ctx context.Context, helmop *fleet.HelmOp, patch crclient.Patch, opts ...interface{}) {
 			// version in status should be the one in the spec
-			if helmop.Status.Version != "1.1.x" {
-				t.Errorf("expecting Status.Version == 1.1.x, got %q", helmop.Status.Version)
+			if helmop.Status.Version != "0.2.0" {
+				t.Errorf("expecting Status.Version == 0.2.0, got %q", helmop.Status.Version)
 			}
 		},
 	).Times(1)
@@ -316,52 +591,10 @@ func TestReconcile_CreatesBundleAndUpdatesStatus(t *testing.T) {
 }
 
 func TestReconcile_ManagePollingJobs(t *testing.T) {
-	helmRepoIndex := `apiVersion: v1
-entries:
-  alpine:
-    - created: 2016-10-06T16:23:20.499814565-06:00
-      description: Deploy a basic Alpine Linux pod
-      digest: 99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd
-      home: https://helm.sh/helm
-      name: alpine
-      sources:
-      - https://github.com/helm/helm
-      urls:
-      - https://technosophos.github.io/tscharts/alpine-0.2.0.tgz
-      version: 0.2.0
-    - created: 2016-10-06T16:23:20.499543808-06:00
-      description: Deploy a basic Alpine Linux pod
-      digest: 515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727
-      home: https://helm.sh/helm
-      name: alpine
-      sources:
-      - https://github.com/helm/helm
-      urls:
-      - https://technosophos.github.io/tscharts/alpine-0.1.0.tgz
-      version: 0.1.0
-  nginx:
-    - created: 2016-10-06T16:23:20.499543808-06:00
-      description: Create a basic nginx HTTP server
-      digest: aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff
-      home: https://helm.sh/helm
-      name: nginx
-      sources:
-      - https://github.com/helm/charts
-      urls:
-      - https://technosophos.github.io/tscharts/nginx-0.1.0.tgz
-      version: 0.1.0
-generated: 2016-10-06T16:23:20.499029981-06:00`
-
-	svr1 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprint(w, helmRepoIndex)
-	}))
+	svr1 := createHelmServer()
 	defer svr1.Close()
 
-	svr2 := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprint(w, helmRepoIndex)
-	}))
+	svr2 := createHelmServer()
 	defer svr2.Close()
 
 	cases := []struct {
@@ -381,6 +614,7 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
+								Repo:    svr1.URL,
 								Chart:   "chart",
 								Version: "1.1.2", // static version
 							},
@@ -405,6 +639,7 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
+								Repo:    svr1.URL,
 								Chart:   "chart",
 								Version: "1.1.2", // static version
 							},
@@ -426,12 +661,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					Namespace: "default",
 				},
 				Spec: fleet.HelmOpSpec{
+					InsecureSkipTLSverify: true,
+					// polling interval not set
 					BundleSpec: fleet.BundleSpec{
-						// polling interval not set
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -451,12 +688,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					Namespace: "default",
 				},
 				Spec: fleet.HelmOpSpec{
+					InsecureSkipTLSverify: true,
+					// polling interval not set
 					BundleSpec: fleet.BundleSpec{
-						// polling interval not set
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -476,12 +715,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					Namespace: "default",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 0 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 0 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -501,12 +742,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					Namespace: "default",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 0 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 0 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -526,12 +769,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					Namespace: "default",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 0 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 0 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -553,12 +798,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					UID:       "foo",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 1 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 1 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -585,12 +832,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					UID:       "foo",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 1 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 1 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},
@@ -611,12 +860,14 @@ generated: 2016-10-06T16:23:20.499029981-06:00`
 					UID:       "foo",
 				},
 				Spec: fleet.HelmOpSpec{
-					PollingInterval: &metav1.Duration{Duration: 1 * time.Minute},
+					InsecureSkipTLSverify: true,
+					PollingInterval:       &metav1.Duration{Duration: 1 * time.Minute},
 					BundleSpec: fleet.BundleSpec{
 						BundleDeploymentOptions: fleet.BundleDeploymentOptions{
 							Helm: &fleet.HelmOptions{
-								Chart:   "chart",
-								Version: "1.x.x",
+								Repo:    svr1.URL,
+								Chart:   "alpine",
+								Version: "0.x.x",
 							},
 						},
 					},