Repository: rancher/fleet
Pull request #2561 (2024-06-27 11:11:46)

Bundle content storage in OCI
This is a rebased version of https://github.com/rancher/fleet/pull/2375 to implement https://github.com/rancher/fleet/issues/2114.



* rename OCIRegistry.URL to OCIRegistry.Reference to match up with https://oras.land/docs/concepts/reference/
* use `setResourceKey`, to set the resource list from BundleDeployments
* add extra status field for OCI reference for information
* use special joinPath instead of filepath.Join, to avoid OS dependent separators
*


Patch:
diff --git a/.github/scripts/deploy-fleet.sh b/.github/scripts/deploy-fleet.sh
index d5894369..f4a65af0 100755
--- a/.github/scripts/deploy-fleet.sh
+++ b/.github/scripts/deploy-fleet.sh
@@ -63,6 +63,8 @@ eventually helm upgrade --install fleet charts/fleet \
   --set apiServerCA="$ca" \
   --set apiServerURL="$server" \
   $shards_settings \
+  --set-string extraEnv[0].name=EXPERIMENTAL_OCI_STORAGE \
+  --set-string extraEnv[0].value=true \
   --set debug=true --set debugLevel=1
 
 # wait for controller and agent rollout
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 95210ebc..c26473db 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -160,6 +160,10 @@ spec:
                   description: DeploymentID is the ID of the currently applied deployment.
                   nullable: true
                   type: string
+                ociContents:
+                  description: OCIContents is true when this deployment's contents
+                    is stored in an oci registry
+                  type: boolean
                 options:
                   description: Options are the deployment options, that are currently
                     applied.
@@ -1261,6 +1265,11 @@ spec:
               type: object
             spec:
               properties:
+                contentsId:
+                  description: ContentsID stores the contents id when deploying contents
+                    using an OCI registry.
+                  nullable: true
+                  type: string
                 correctDrift:
                   description: CorrectDrift specifies how drift correction should
                     work.
@@ -2609,6 +2618,12 @@ spec:
                     bundle.
                   format: int64
                   type: integer
+                ociReference:
+                  description: 'OCIReference is the OCI reference used to store contents,
+                    this is
+
+                    only for informational purposes.'
+                  type: string
                 partitions:
                   description: PartitionStatus lists the status of each partition.
                   items:
@@ -5832,6 +5847,28 @@ spec:
                   description: KeepResources specifies if the resources created must
                     be kept after deleting the GitRepo.
                   type: boolean
+                ociRegistry:
+                  description: OCIRegistry specifies the OCI registry related parameters
+                  properties:
+                    authSecretName:
+                      description: AuthSecretName contains the auth secret where the
+                        OCI regristry credentials are stored.
+                      nullable: true
+                      type: string
+                    basicHTTP:
+                      description: BasicHTTP uses HTTP connections to the OCI registry
+                        when enabled.
+                      nullable: true
+                      type: boolean
+                    insecureSkipTLS:
+                      description: InsecureSkipTLS allows connections to OCI registry
+                        without certs when enabled.
+                      nullable: true
+                      type: boolean
+                    reference:
+                      description: Reference of the OCI Registry
+                      type: string
+                  type: object
                 paths:
                   description: 'Paths is the directories relative to the git repo
                     root that contain resources to be applied.
diff --git a/dev/setup-fleet b/dev/setup-fleet
index ee912ebe..adc50791 100755
--- a/dev/setup-fleet
+++ b/dev/setup-fleet
@@ -40,6 +40,8 @@ helm -n cattle-fleet-system upgrade --install --create-namespace --wait --reset-
   --set apiServerCA="$ca" \
   --set apiServerURL="$server" \
   $shards_settings \
+  --set-string extraEnv[0].name=EXPERIMENTAL_OCI_STORAGE \
+  --set-string extraEnv[0].value=true \
   --set debug=true --set debugLevel=1 fleet charts/fleet
 
 # wait for controller and agent rollout
diff --git a/go.mod b/go.mod
index ec5b7386..1436f660 100644
--- a/go.mod
+++ b/go.mod
@@ -29,6 +29,8 @@ require (
 	github.com/jpillora/backoff v1.0.0
 	github.com/onsi/ginkgo/v2 v2.19.0
 	github.com/onsi/gomega v1.33.1
+	github.com/opencontainers/go-digest v1.0.0
+	github.com/opencontainers/image-spec v1.1.0
 	github.com/otiai10/copy v1.14.0
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.19.1
@@ -58,6 +60,8 @@ require (
 	k8s.io/kube-openapi v0.0.0-20240620174524-b456828f718b
 	k8s.io/kubectl v0.29.4
 	k8s.io/kubernetes v1.29.4
+	k8s.io/utils v0.0.0-20240102154912-e7106e64919e
+	oras.land/oras-go/v2 v2.5.0
 	sigs.k8s.io/cli-utils v0.34.0
 	sigs.k8s.io/controller-runtime v0.17.2
 	sigs.k8s.io/controller-tools v0.14.0
@@ -181,8 +185,6 @@ require (
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
-	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -242,7 +244,6 @@ require (
 	k8s.io/code-generator v0.29.4 // indirect
 	k8s.io/component-base v0.29.4 // indirect
 	k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect
-	k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
 	oras.land/oras-go v1.2.5 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
diff --git a/go.sum b/go.sum
index f950e89f..caa1f7f7 100644
--- a/go.sum
+++ b/go.sum
@@ -1498,6 +1498,8 @@ k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCf
 k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
 oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
+oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
+oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/internal/cmd/agent/deployer/deployer.go b/internal/cmd/agent/deployer/deployer.go
index 8e1cae2c..5785203f 100644
--- a/internal/cmd/agent/deployer/deployer.go
+++ b/internal/cmd/agent/deployer/deployer.go
@@ -5,10 +5,12 @@ import (
 	"fmt"
 	"reflect"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/rancher/fleet/internal/helmdeployer"
 	"github.com/rancher/fleet/internal/manifest"
+	"github.com/rancher/fleet/internal/ociwrapper"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
 	"github.com/rancher/wrangler/v2/pkg/condition"
@@ -105,9 +107,48 @@ func (d *Deployer) helmdeploy(ctx context.Context, bd *fleet.BundleDeployment) (
 		}
 	}
 	manifestID, _ := kv.Split(bd.Spec.DeploymentID, ":")
-	manifest, err := d.lookup.Get(ctx, d.upstreamClient, manifestID)
-	if err != nil {
-		return "", err
+	var (
+		manifest *manifest.Manifest
+		err      error
+	)
+	if bd.Spec.OCIContents {
+		// First we need to access the secret where the OCI registry reference and credentials are located
+		var secret corev1.Secret
+		secretID := types.NamespacedName{Name: manifestID, Namespace: bd.Namespace}
+		if err := d.upstreamClient.Get(ctx, secretID, &secret); err != nil {
+			return "", err
+		}
+		ref, ok := secret.Data[ociwrapper.OCISecretReference]
+		if !ok {
+			return "", fmt.Errorf("expected data [reference] not found in secret: %s", manifestID)
+		}
+		username := string(secret.Data[ociwrapper.OCISecretUsername])
+		password := string(secret.Data[ociwrapper.OCISecretPassword])
+		basicHttp, err := strconv.ParseBool(string(secret.Data[ociwrapper.OCISecretBasicHTTP]))
+		if err != nil {
+			return "", fmt.Errorf("value for [ociRegistry.http] in secret %s cannot be parsed as boolean", manifestID)
+		}
+		insecure, err := strconv.ParseBool(string(secret.Data[ociwrapper.OCISecretInsecure]))
+		if err != nil {
+			return "", fmt.Errorf("value for [ociRegistry.insecure] in secret %s cannot be parsed as boolean", manifestID)
+		}
+		ociOpts := ociwrapper.OCIOpts{
+			Reference:       string(ref),
+			Username:        username,
+			Password:        password,
+			BasicHTTP:       basicHttp,
+			InsecureSkipTLS: insecure,
+		}
+		oci := ociwrapper.NewOCIWrapper()
+		manifest, err = oci.PullManifest(ctx, ociOpts, manifestID)
+		if err != nil {
+			return "", err
+		}
+	} else {
+		manifest, err = d.lookup.Get(ctx, d.upstreamClient, manifestID)
+		if err != nil {
+			return "", err
+		}
 	}
 
 	manifest.Commit = bd.Labels["fleet.cattle.io/commit"]
diff --git a/internal/cmd/agent/register/register.go b/internal/cmd/agent/register/register.go
index 666c1546..f4218cd2 100644
--- a/internal/cmd/agent/register/register.go
+++ b/internal/cmd/agent/register/register.go
@@ -59,6 +59,20 @@ func Get(ctx context.Context, namespace string, cfg *rest.Config) (*AgentInfo, e
 		return nil, err
 	}
 
+	agentConfig, err := config.Lookup(ctx, namespace, config.AgentConfigName, k8s.Core().V1().ConfigMap())
+	if err != nil {
+		return nil, fmt.Errorf(
+			"failed to look up client config %s/%s: %w",
+			namespace,
+			config.AgentConfigName,
+			err,
+		)
+	}
+
+	if agentConfig.AgentTLSMode == config.AgentTLSModeStrict {
+		config.BypassSystemCAStore()
+	}
+
 	secret, err := k8s.Core().V1().Secret().Get(namespace, CredName, metav1.GetOptions{})
 	if apierrors.IsNotFound(err) {
 		logrus.Warn("Cannot find fleet-agent secret")
@@ -326,22 +340,12 @@ func createClientConfigFromSecret(secret *corev1.Secret, trustSystemStoreCAs boo
 	token := string(data[Token])
 
 	if trustSystemStoreCAs { // Save a request to the API server URL if system CAs are not to be trusted.
+		// NOTE(manno): client-go will use the system trust store even if a CA is configured. So, why do this?
 		if _, err := http.Get(apiServerURL); err == nil {
 			apiServerCA = nil
 		}
 	} else {
-		// Bypass the OS trust store through env vars, see https://pkg.go.dev/crypto/x509#SystemCertPool
-		// We set values to paths belonging to the root filesystem, which is read-only, to prevent tampering.
-		// Note: this will not work on Windows nor Mac OS. Agent are expected to run on Linux nodes.
-		err := os.Setenv("SSL_CERT_FILE", "/dev/null")
-		if err != nil {
-			logrus.Errorf("failed to set env var SSL_CERT_FILE: %s", err.Error())
-		}
-
-		err = os.Setenv("SSL_CERT_DIR", "/dev/null")
-		if err != nil {
-			logrus.Errorf("failed to set env var SSL_CERT_DIR: %s", err.Error())
-		}
+		config.BypassSystemCAStore()
 	}
 
 	cfg := clientcmdapi.Config{
diff --git a/internal/cmd/cli/apply.go b/internal/cmd/cli/apply.go
index 35cc9aa7..b41f0880 100644
--- a/internal/cmd/cli/apply.go
+++ b/internal/cmd/cli/apply.go
@@ -51,6 +51,11 @@ type Apply struct {
 	CorrectDrift                bool              `usage:"Rollback any change made from outside of Fleet" name:"correct-drift"`
 	CorrectDriftForce           bool              `usage:"Use --force when correcting drift. Resources can be deleted and recreated" name:"correct-drift-force"`
 	CorrectDriftKeepFailHistory bool              `usage:"Keep helm history for failed rollbacks" name:"correct-drift-keep-fail-history"`
+	OCIReference                string            `usage:"OCI registry reference" name:"oci-reference"`
+	OCIUsername                 string            `usage:"Basic auth username for OCI registry" env:"OCI_USERNAME"`
+	OCIPasswordFile             string            `usage:"Path of file containing basic auth password for OCI registry" name:"oci-password-file"`
+	OCIBasicHTTP                bool              `usage:"Use HTTP to access the OCI regustry" name:"oci-basic-http"`
+	OCIInsecure                 bool              `usage:"Allow connections to OCI registry without certs" name:"oci-insecure"`
 }
 
 func (r *Apply) PersistentPre(_ *cobra.Command, _ []string) error {
@@ -91,8 +96,10 @@ func (a *Apply) Run(cmd *cobra.Command, args []string) error {
 		CorrectDriftForce:           a.CorrectDriftForce,
 		CorrectDriftKeepFailHistory: a.CorrectDriftKeepFailHistory,
 	}
-	err := a.addAuthToOpts(&opts, os.ReadFile)
-	if err != nil {
+	if err := a.addAuthToOpts(&opts, os.ReadFile); err != nil {
+		return err
+	}
+	if err := a.addOCISpecToOpts(&opts, os.ReadFile); err != nil {
 		return err
 	}
 	if a.File == "-" {
@@ -168,6 +175,29 @@ func (a *Apply) addAuthToOpts(opts *apply.Options, readFile readFile) error {
 	return nil
 }
 
+// addOCISpecToOpts adds the OCI registry specs (with auth if provided)
+func (a *Apply) addOCISpecToOpts(opts *apply.Options, readFile readFile) error {
+	// returning if the OCI registry reference is not defined
+	if a.OCIReference == "" {
+		return nil
+	}
+	opts.OCIRegistry.Reference = a.OCIReference
+
+	if a.OCIUsername != "" && a.OCIPasswordFile != "" {
+		password, err := readFile(a.OCIPasswordFile)
+		if err != nil {
+			return err
+		}
+
+		opts.OCIRegistry.Username = a.OCIUsername
+		opts.OCIRegistry.Password = string(password)
+	}
+	opts.OCIRegistry.BasicHTTP = a.OCIBasicHTTP
+	opts.OCIRegistry.InsecureSkipTLS = a.OCIInsecure
+
+	return nil
+}
+
 func currentCommit() string {
 	cmd := exec.Command("git", "rev-parse", "HEAD")
 	buf := &bytes.Buffer{}
diff --git a/internal/cmd/cli/apply/apply.go b/internal/cmd/cli/apply/apply.go
index 7aefb3f9..1234280a 100644
--- a/internal/cmd/cli/apply/apply.go
+++ b/internal/cmd/cli/apply/apply.go
@@ -9,6 +9,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	"github.com/sirupsen/logrus"
@@ -16,15 +17,19 @@ import (
 	"github.com/rancher/fleet/internal/bundlereader"
 	"github.com/rancher/fleet/internal/client"
 	"github.com/rancher/fleet/internal/fleetyaml"
+	"github.com/rancher/fleet/internal/manifest"
 	name2 "github.com/rancher/fleet/internal/name"
+	"github.com/rancher/fleet/internal/ociwrapper"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
 	"github.com/rancher/wrangler/v2/pkg/yaml"
 
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/utils/ptr"
 )
 
 var (
@@ -36,6 +41,14 @@ type Getter interface {
 	GetNamespace() string
 }
 
+type OCIRegistrySpec struct {
+	Reference       string
+	Username        string
+	Password        string
+	BasicHTTP       bool
+	InsecureSkipTLS bool
+}
+
 type Options struct {
 	BundleFile                  string
 	TargetsFile                 string
@@ -55,6 +68,7 @@ type Options struct {
 	CorrectDrift                bool
 	CorrectDriftForce           bool
 	CorrectDriftKeepFailHistory bool
+	OCIRegistry                 OCIRegistrySpec
 }
 
 func globDirs(baseDir string) (result []string, err error) {
@@ -227,7 +241,7 @@ func Dir(ctx context.Context, client Getter, name, baseDir string, opts *Options
 	}
 
 	if opts.Output == nil {
-		err = save(client, def, scans...)
+		err = save(ctx, client, def, opts, scans...)
 	} else {
 		_, err = opts.Output.Write(b)
 	}
@@ -235,7 +249,28 @@ func Dir(ctx context.Context, client Getter, name, baseDir string, opts *Options
 	return err
 }
 
-func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) error {
+func pushOCIManifest(ctx context.Context, bundle *fleet.Bundle, opts *Options) (string, error) {
+	manifest := manifest.FromBundle(bundle)
+	manifestID, err := manifest.ID()
+	if err != nil {
+		return "", err
+	}
+	ociOpts := ociwrapper.OCIOpts{
+		Reference:       opts.OCIRegistry.Reference,
+		Username:        opts.OCIRegistry.Username,
+		Password:        opts.OCIRegistry.Password,
+		BasicHTTP:       opts.OCIRegistry.BasicHTTP,
+		InsecureSkipTLS: opts.OCIRegistry.InsecureSkipTLS,
+	}
+	oci := ociwrapper.NewOCIWrapper()
+	err = oci.PushManifest(ctx, ociOpts, manifestID, manifest)
+	if err != nil {
+		return "", err
+	}
+	return manifestID, nil
+}
+
+func save(ctx context.Context, client Getter, bundle *fleet.Bundle, opts *Options, imageScans ...*fleet.ImageScan) error {
 	c, err := client.Get()
 	if err != nil {
 		return err
@@ -243,8 +278,14 @@ func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) e
 
 	obj, err := c.Fleet.Bundle().Get(bundle.Namespace, bundle.Name, metav1.GetOptions{})
 	if apierrors.IsNotFound(err) {
-		if _, err = c.Fleet.Bundle().Create(bundle); err != nil {
-			return err
+		if opts.OCIRegistry.Reference != "" {
+			if err := saveOCIBundle(ctx, c, bundle, opts); err != nil {
+				return err
+			}
+		} else {
+			if _, err := c.Fleet.Bundle().Create(bundle); err != nil {
+				return err
+			}
 		}
 		logrus.Infof("created: %s/%s", bundle.Namespace, bundle.Name)
 	} else if err != nil {
@@ -281,6 +322,54 @@ func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) e
 			logrus.Infof("updated (scan): %s/%s", obj.Namespace, obj.Name)
 		}
 	}
+	return nil
+}
+
+func saveOCIBundle(ctx context.Context, c *client.Client, bundle *fleet.Bundle, opts *Options) error {
+	manifestID, err := pushOCIManifest(ctx, bundle, opts)
+	if err != nil {
+		return err
+	}
+	// OCI artifact stored successfully
+	// We don't store the resources in the bundle. Just keep the manifestID for
+	// being able to access the bundle's contents later.
+	bundle.Spec.Resources = nil
+	bundle.Spec.ContentsID = manifestID
+
+	bundleCreated, err := c.Fleet.Bundle().Create(bundle)
+	if err != nil {
+		return err
+	}
+	// when using the OCI registry manifestID won't be empty
+	// In this case we need to create a secret to store the
+	// OCI registry reference and credentials so the fleet controller is
+	// able to access.
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      manifestID,
+			Namespace: bundle.Namespace,
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					APIVersion:         fleet.SchemeGroupVersion.String(),
+					Kind:               "Bundle",
+					Name:               bundleCreated.GetName(),
+					UID:                bundleCreated.GetUID(),
+					BlockOwnerDeletion: ptr.To(true),
+					Controller:         ptr.To(true),
+				},
+			},
+		},
+		Data: map[string][]byte{
+			ociwrapper.OCISecretReference: []byte(opts.OCIRegistry.Reference),
+			ociwrapper.OCISecretUsername:  []byte(opts.OCIRegistry.Username),
+			ociwrapper.OCISecretPassword:  []byte(opts.OCIRegistry.Password),
+			ociwrapper.OCISecretBasicHTTP: []byte(strconv.FormatBool(opts.OCIRegistry.BasicHTTP)),
+			ociwrapper.OCISecretInsecure:  []byte(strconv.FormatBool(opts.OCIRegistry.InsecureSkipTLS)),
+		},
+	}
+	if _, err := c.Core.Secret().Create(secret); err != nil {
+		return err
+	}
 	return err
 }
 
diff --git a/internal/cmd/cli/gitcloner/cloner.go b/internal/cmd/cli/gitcloner/cloner.go
index 511c5e32..d0e63f17 100644
--- a/internal/cmd/cli/gitcloner/cloner.go
+++ b/internal/cmd/cli/gitcloner/cloner.go
@@ -73,12 +73,13 @@ func (c *Cloner) CloneRepo(opts *GitCloner) error {
 
 func cloneBranch(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
 	_, err := plainClone(opts.Path, false, &git.CloneOptions{
-		URL:             opts.Repo,
-		Auth:            auth,
-		InsecureSkipTLS: opts.InsecureSkipTLS,
-		CABundle:        caBundle,
-		SingleBranch:    true,
-		ReferenceName:   plumbing.ReferenceName(opts.Branch),
+		URL:               opts.Repo,
+		Auth:              auth,
+		InsecureSkipTLS:   opts.InsecureSkipTLS,
+		CABundle:          caBundle,
+		SingleBranch:      true,
+		ReferenceName:     plumbing.ReferenceName(opts.Branch),
+		RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 	})
 
 	return err
@@ -86,10 +87,11 @@ func cloneBranch(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) er
 
 func cloneRevision(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
 	r, err := plainClone(opts.Path, false, &git.CloneOptions{
-		URL:             opts.Repo,
-		Auth:            auth,
-		InsecureSkipTLS: opts.InsecureSkipTLS,
-		CABundle:        caBundle,
+		URL:               opts.Repo,
+		Auth:              auth,
+		InsecureSkipTLS:   opts.InsecureSkipTLS,
+		CABundle:          caBundle,
+		RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 	})
 	if err != nil {
 		return err
diff --git a/internal/cmd/controller/agentmanagement/controllers/resources/data.go b/internal/cmd/controller/agentmanagement/controllers/resources/data.go
index fd347b8d..a0ce8507 100644
--- a/internal/cmd/controller/agentmanagement/controllers/resources/data.go
+++ b/internal/cmd/controller/agentmanagement/controllers/resources/data.go
@@ -34,6 +34,11 @@ func ApplyBootstrapResources(systemNamespace, systemRegistrationNamespace string
 					APIGroups: []string{fleet.SchemeGroupVersion.Group},
 					Resources: []string{fleet.BundleDeploymentResourceNamePlural + "/status"},
 				},
+				{
+					Verbs:     []string{"get"},
+					APIGroups: []string{""},
+					Resources: []string{"secrets"},
+				},
 			},
 		},
 		// used by request-* service accounts from agents
diff --git a/internal/cmd/controller/gitops/reconciler/gitjob_controller.go b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
index 8ddfa7ee..369611bb 100644
--- a/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
@@ -13,6 +13,7 @@ import (
 	"github.com/rancher/fleet/internal/cmd/controller/grutil"
 	"github.com/rancher/fleet/internal/cmd/controller/imagescan"
 	"github.com/rancher/fleet/internal/metrics"
+	"github.com/rancher/fleet/internal/ociwrapper"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/git"
 	"github.com/rancher/fleet/pkg/sharding"
@@ -40,12 +41,13 @@ import (
 )
 
 const (
-	bundleCAVolumeName      = "additional-ca"
-	bundleCAFile            = "additional-ca.crt"
-	gitCredentialVolumeName = "git-credential" // #nosec G101 this is not a credential
-	gitClonerVolumeName     = "git-cloner"
-	emptyDirVolumeName      = "git-cloner-empty-dir"
-	fleetHomeDir            = "/fleet-home"
+	bundleCAVolumeName        = "additional-ca"
+	bundleCAFile              = "additional-ca.crt"
+	gitCredentialVolumeName   = "git-credential" // #nosec G101 this is not a credential
+	ociRegistryAuthVolumeName = "oci-auth"
+	gitClonerVolumeName       = "git-cloner"
+	emptyDirVolumeName        = "git-cloner-empty-dir"
+	fleetHomeDir              = "/fleet-home"
 )
 
 var two = int32(2)
@@ -546,6 +548,20 @@ func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.Git
 		volumeMounts = append(volumeMounts, volMnts...)
 	}
 
+	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
+		vol, volMnt, err := ociVolumeFromSecret(ctx, r.Client,
+			gitrepo.Namespace,
+			gitrepo.Spec.OCIRegistry.AuthSecretName,
+			ociRegistryAuthVolumeName,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		volumes = append(volumes, vol)
+		volumeMounts = append(volumeMounts, volMnt)
+	}
+
 	saName := name.SafeConcatName("git", gitrepo.Name)
 	logger := log.FromContext(ctx)
 	args, envs := argsAndEnvs(gitrepo, logger.V(1).Enabled())
@@ -691,6 +707,32 @@ func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool) ([]string, []corev1.EnvV
 			})
 	}
 
+	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.Reference != "" {
+		args = append(args, "--oci-reference", gitrepo.Spec.OCIRegistry.Reference)
+		if gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
+			args = append(args, "--oci-password-file", "/etc/fleet/oci/password")
+			env = append(env,
+				corev1.EnvVar{
+					Name: "OCI_USERNAME",
+					ValueFrom: &corev1.EnvVarSource{
+						SecretKeyRef: &corev1.SecretKeySelector{
+							Optional: &[]bool{true}[0],
+							Key:      "username",
+							LocalObjectReference: corev1.LocalObjectReference{
+								Name: gitrepo.Spec.OCIRegistry.AuthSecretName,
+							},
+						},
+					},
+				})
+		}
+		if gitrepo.Spec.OCIRegistry.BasicHTTP {
+			args = append(args, "--oci-basic-http")
+		}
+		if gitrepo.Spec.OCIRegistry.InsecureSkipTLS {
+			args = append(args, "--oci-insecure")
+		}
+	}
+
 	return append(args, "--", gitrepo.Name), env
 }
 
@@ -745,6 +787,34 @@ func volumes(targetsConfigName string) ([]corev1.Volume, []corev1.VolumeMount) {
 	return volumes, volumeMounts
 }
 
+// ociVolumeFromSecret generates a volume and volume mount from a basic-auth secret.
+func ociVolumeFromSecret(
+	ctx context.Context,
+	c client.Client,
+	namespace, secretName, volumeName string,
+) (corev1.Volume, corev1.VolumeMount, error) {
+	var secret corev1.Secret
+	if err := c.Get(ctx, types.NamespacedName{
+		Namespace: namespace,
+		Name:      secretName,
+	}, &secret); err != nil {
+		return corev1.Volume{}, corev1.VolumeMount{}, err
+	}
+	volume := corev1.Volume{
+		Name: volumeName,
+		VolumeSource: corev1.VolumeSource{
+			Secret: &corev1.SecretVolumeSource{
+				SecretName: secretName,
+			},
+		},
+	}
+	volumeMount := corev1.VolumeMount{
+		Name:      volumeName,
+		MountPath: "/etc/fleet/oci",
+	}
+	return volume, volumeMount, nil
+}
+
 // volumesFromSecret generates volumes and volume mounts from a Helm secret, assuming that that secret exists.
 // If the secret has a cacerts key, it will be mounted into /etc/ssl/certs, too.
 func volumesFromSecret(
diff --git a/internal/cmd/controller/grutil/gitjob.go b/internal/cmd/controller/grutil/gitjob.go
index 2e737c85..17efeb8f 100644
--- a/internal/cmd/controller/grutil/gitjob.go
+++ b/internal/cmd/controller/grutil/gitjob.go
@@ -34,6 +34,11 @@ func NewRole(namespace string, name string) *rbacv1.Role {
 				APIGroups: []string{"fleet.cattle.io"},
 				Resources: []string{"gitrepos"},
 			},
+			{
+				Verbs:     []string{"create"},
+				APIGroups: []string{""},
+				Resources: []string{"secrets"},
+			},
 		},
 	}
 }
diff --git a/internal/cmd/controller/reconciler/bundle_controller.go b/internal/cmd/controller/reconciler/bundle_controller.go
index 9882ef8b..34b0692c 100644
--- a/internal/cmd/controller/reconciler/bundle_controller.go
+++ b/internal/cmd/controller/reconciler/bundle_controller.go
@@ -4,16 +4,20 @@ package reconciler
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/rancher/fleet/internal/cmd/controller/finalize"
 	"github.com/rancher/fleet/internal/cmd/controller/summary"
 	"github.com/rancher/fleet/internal/cmd/controller/target"
 	"github.com/rancher/fleet/internal/manifest"
 	"github.com/rancher/fleet/internal/metrics"
+	"github.com/rancher/fleet/internal/ociwrapper"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/sharding"
 
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/util/retry"
@@ -173,21 +177,26 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 
 	logger.V(1).Info("Reconciling bundle, checking targets, calculating changes, building objects", "generation", bundle.Generation, "observedGeneration", bundle.Status.ObservedGeneration)
 
-	manifest := manifest.FromBundle(bundle)
-	if bundle.Generation != bundle.Status.ObservedGeneration {
-		manifest.ResetSHASum()
-	}
+	contentsInOCI := bundle.Spec.ContentsID != "" && ociwrapper.ExperimentalOCIIsEnabled()
+	manifestID := bundle.Spec.ContentsID
+	var resourcesManifest *manifest.Manifest
+	if !contentsInOCI {
+		resourcesManifest = manifest.FromBundle(bundle)
+		if bundle.Generation != bundle.Status.ObservedGeneration {
+			resourcesManifest.ResetSHASum()
+		}
 
-	manifestDigest, err := manifest.SHASum()
-	if err != nil {
-		return ctrl.Result{}, err
-	}
-	bundle.Status.ResourcesSHA256Sum = manifestDigest
+		manifestDigest, err := resourcesManifest.SHASum()
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+		bundle.Status.ResourcesSHA256Sum = manifestDigest
 
-	manifestID, err := manifest.ID()
-	if err != nil {
-		// this should never happen, since manifest.SHASum() cached the result and worked above.
-		return ctrl.Result{}, err
+		manifestID, err = resourcesManifest.ID()
+		if err != nil {
+			// this should never happen, since manifest.SHASum() cached the result and worked above.
+			return ctrl.Result{}, err
+		}
 	}
 
 	matchedTargets, err := r.Builder.Targets(ctx, bundle, manifestID)
@@ -195,9 +204,11 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, err
 	}
 
-	// do not create a content resource if there are no targets, it will
-	// only create work for `PurgeOrphanedInBackground`.
-	if len(matchedTargets) > 0 {
+	if !contentsInOCI && len(matchedTargets) > 0 {
+		// when not using the OCI registry we need to create a contents resource
+		// so the BundleDeployments are able to access the contents to be deployed.
+		// Otherwise, do not create a content resource if there are no targets, it will
+		// only create work for `PurgeOrphanedInBackground`.
 		// `fleet apply` puts all resources into `bundle.Spec.Resources`.
 		// `Store` copies all the resources into the content resource.
 		// There is no pruning of unused resources. Therefore we write
@@ -206,7 +217,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		// the `helm.Chart` field, change which resources are used. The
 		// agents have access to all resources and use their specific
 		// set of `BundleDeploymentOptions`.
-		err := r.Store.Store(ctx, manifest)
+		err := r.Store.Store(ctx, resourcesManifest)
 		if err != nil {
 			return ctrl.Result{}, err
 		}
@@ -221,6 +232,14 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, err
 	}
 
+	if contentsInOCI {
+		url, err := r.getOCIReference(ctx, bundle)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+		bundle.Status.OCIReference = url
+	}
+
 	setResourceKey(&bundle.Status, matchedTargets)
 
 	summary.SetReadyConditions(&bundle.Status, "Cluster", bundle.Status.Summary)
@@ -246,6 +265,8 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		// been created.
 		controllerutil.AddFinalizer(bd, bundleDeploymentFinalizer)
 
+		bd.Spec.OCIContents = contentsInOCI
+
 		updated := bd.DeepCopy()
 		op, err := controllerutil.CreateOrUpdate(ctx, r.Client, bd, func() error {
 			bd.Spec = updated.Spec
@@ -257,6 +278,13 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 			return ctrl.Result{}, err
 		}
 		logger.V(1).Info(upper(op)+" bundledeployment", "bundledeployment", bd, "operation", op)
+
+		if contentsInOCI {
+			// we need to create the OCI registry credentials secret in the BundleDeployment's namespace
+			if err := r.createDeploymentOCISecret(ctx, bundle, bd); err != nil {
+				return ctrl.Result{}, err
+			}
+		}
 	}
 
 	updateDisplay(&bundle.Status)
@@ -295,3 +323,53 @@ func upper(op controllerutil.OperationResult) string {
 		return "Unknown"
 	}
 }
+
+func (r *BundleReconciler) createDeploymentOCISecret(ctx context.Context, bundle *fleet.Bundle, bd *fleet.BundleDeployment) error {
+	namespacedName := types.NamespacedName{
+		Namespace: bundle.Namespace,
+		Name:      bundle.Spec.ContentsID,
+	}
+	var ociSecret corev1.Secret
+	if err := r.Get(ctx, namespacedName, &ociSecret); err != nil {
+		return err
+	}
+	// clone the secret, and just change the namespace so it's in the target's namespace
+	targetOCISecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      ociSecret.Name,
+			Namespace: bd.Namespace,
+		},
+		Data: ociSecret.Data,
+	}
+
+	if err := controllerutil.SetControllerReference(bd, targetOCISecret, r.Scheme); err != nil {
+		return err
+	}
+	if err := r.Create(ctx, targetOCISecret); err != nil {
+		if !apierrors.IsAlreadyExists(err) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (r *BundleReconciler) getOCIReference(ctx context.Context, bundle *fleet.Bundle) (string, error) {
+	if bundle.Spec.ContentsID == "" {
+		return "", fmt.Errorf("cannot get OCI reference. Bundles's ContentsID is not set")
+	}
+	namespacedName := types.NamespacedName{
+		Namespace: bundle.Namespace,
+		Name:      bundle.Spec.ContentsID,
+	}
+	var ociSecret corev1.Secret
+	if err := r.Get(ctx, namespacedName, &ociSecret); err != nil {
+		return "", err
+	}
+	ref, ok := ociSecret.Data[ociwrapper.OCISecretReference]
+	if !ok {
+		return "", fmt.Errorf("expected data [reference] not found in secret: %s", bundle.Spec.ContentsID)
+	}
+	// this is not a valid reference, it is only for display
+	return fmt.Sprintf("oci://%s/%s:latest", string(ref), bundle.Spec.ContentsID), nil
+}
diff --git a/internal/config/overrides.go b/internal/config/overrides.go
new file mode 100644
index 00000000..f48c6d41
--- /dev/null
+++ b/internal/config/overrides.go
@@ -0,0 +1,25 @@
+package config
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+// BypassSystemCAStore is used to bypass the OS trust store in agents through env vars, see
+// https://pkg.go.dev/crypto/x509#SystemCertPool for more info.
+// We set values to paths belonging to the root filesystem, which is read-only, to prevent tampering.
+// Eventually, this should not be necessary, if/when we find a way to set client-go's API Config to achieve similar
+// effects.
+// Note: this will not work on Windows nor Mac OS. Agents are expected to run on Linux nodes.
+func BypassSystemCAStore() {
+	err := os.Setenv("SSL_CERT_FILE", "/dev/null")
+	if err != nil {
+		logrus.Errorf("failed to set env var SSL_CERT_FILE: %s", err.Error())
+	}
+
+	err = os.Setenv("SSL_CERT_DIR", "/dev/null")
+	if err != nil {
+		logrus.Errorf("failed to set env var SSL_CERT_DIR: %s", err.Error())
+	}
+}
diff --git a/internal/mocks/orastarget_mock.go b/internal/mocks/orastarget_mock.go
new file mode 100644
index 00000000..20e28370
--- /dev/null
+++ b/internal/mocks/orastarget_mock.go
@@ -0,0 +1,110 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: oras.land/oras-go/v2 (interfaces: Target)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	io "io"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1 "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// MockTarget is a mock of Target interface.
+type MockTarget struct {
+	ctrl     *gomock.Controller
+	recorder *MockTargetMockRecorder
+}
+
+// MockTargetMockRecorder is the mock recorder for MockTarget.
+type MockTargetMockRecorder struct {
+	mock *MockTarget
+}
+
+// NewMockTarget creates a new mock instance.
+func NewMockTarget(ctrl *gomock.Controller) *MockTarget {
+	mock := &MockTarget{ctrl: ctrl}
+	mock.recorder = &MockTargetMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockTarget) EXPECT() *MockTargetMockRecorder {
+	return m.recorder
+}
+
+// Exists mocks base method.
+func (m *MockTarget) Exists(arg0 context.Context, arg1 v1.Descriptor) (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Exists", arg0, arg1)
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Exists indicates an expected call of Exists.
+func (mr *MockTargetMockRecorder) Exists(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockTarget)(nil).Exists), arg0, arg1)
+}
+
+// Fetch mocks base method.
+func (m *MockTarget) Fetch(arg0 context.Context, arg1 v1.Descriptor) (io.ReadCloser, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Fetch", arg0, arg1)
+	ret0, _ := ret[0].(io.ReadCloser)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Fetch indicates an expected call of Fetch.
+func (mr *MockTargetMockRecorder) Fetch(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockTarget)(nil).Fetch), arg0, arg1)
+}
+
+// Push mocks base method.
+func (m *MockTarget) Push(arg0 context.Context, arg1 v1.Descriptor, arg2 io.Reader) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Push", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Push indicates an expected call of Push.
+func (mr *MockTargetMockRecorder) Push(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockTarget)(nil).Push), arg0, arg1, arg2)
+}
+
+// Resolve mocks base method.
+func (m *MockTarget) Resolve(arg0 context.Context, arg1 string) (v1.Descriptor, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Resolve", arg0, arg1)
+	ret0, _ := ret[0].(v1.Descriptor)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Resolve indicates an expected call of Resolve.
+func (mr *MockTargetMockRecorder) Resolve(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockTarget)(nil).Resolve), arg0, arg1)
+}
+
+// Tag mocks base method.
+func (m *MockTarget) Tag(arg0 context.Context, arg1 v1.Descriptor, arg2 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Tag", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Tag indicates an expected call of Tag.
+func (mr *MockTargetMockRecorder) Tag(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tag", reflect.TypeOf((*MockTarget)(nil).Tag), arg0, arg1, arg2)
+}
diff --git a/internal/ociwrapper/ociwrapper.go b/internal/ociwrapper/ociwrapper.go
new file mode 100644
index 00000000..ec73f98a
--- /dev/null
+++ b/internal/ociwrapper/ociwrapper.go
@@ -0,0 +1,266 @@
+package ociwrapper
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/rancher/fleet/internal/manifest"
+	"oras.land/oras-go/v2"
+	"oras.land/oras-go/v2/content"
+	"oras.land/oras-go/v2/content/memory"
+	"oras.land/oras-go/v2/registry/remote"
+	"oras.land/oras-go/v2/registry/remote/auth"
+	"oras.land/oras-go/v2/registry/remote/retry"
+)
+
+const (
+	fileType     = "application/fleet.file"
+	artifactType = "application/fleet.manifest"
+
+	OCISecretUsername  = "username"
+	OCISecretPassword  = "password"
+	OCISecretReference = "reference"
+	OCISecretBasicHTTP = "http"
+	OCISecretInsecure  = "insecure"
+)
+
+type OCIOpts struct {
+	Reference       string
+	Username        string
+	Password        string
+	BasicHTTP       bool
+	InsecureSkipTLS bool
+}
+
+type OrasOps interface {
+	PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion oras.PackManifestVersion, artifactType string, opts oras.PackManifestOptions) (ocispec.Descriptor, error)
+	Copy(ctx context.Context, src oras.ReadOnlyTarget, srcRef string, dst oras.Target, dstRef string, opts oras.CopyOptions) (ocispec.Descriptor, error)
+	NewStore() oras.Target
+}
+
+type OrasOperator struct{}
+
+func (o *OrasOperator) NewStore() oras.Target {
+	return memory.New()
+}
+
+func (o *OrasOperator) PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion oras.PackManifestVersion, artifactType string, opts oras.PackManifestOptions) (ocispec.Descriptor, error) {
+	return oras.PackManifest(ctx, pusher, packManifestVersion, artifactType, opts)
+}
+
+func (o *OrasOperator) Copy(ctx context.Context, src oras.ReadOnlyTarget, srcRef string, dst oras.Target, dstRef string, opts oras.CopyOptions) (ocispec.Descriptor, error) {
+	return oras.Copy(ctx, src, srcRef, dst, dstRef, opts)
+}
+
+type OCIWrapper struct {
+	oci OrasOps
+}
+
+func NewOCIWrapper() *OCIWrapper {
+	return &OCIWrapper{
+		oci: &OrasOperator{},
+	}
+}
+
+func getHTTPClient(insecureSkipTLS bool) *http.Client {
+	if !insecureSkipTLS {
+		return retry.DefaultClient
+	}
+	return &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402
+		},
+	}
+}
+
+func getAuthClient(opts OCIOpts) *auth.Client {
+	client := &auth.Client{
+		Client: getHTTPClient(opts.InsecureSkipTLS),
+		Cache:  auth.NewCache(),
+	}
+	if opts.Username != "" {
+		cred := auth.Credential{
+			Username: opts.Username,
+			Password: opts.Password,
+		}
+		client.Credential = func(ctx context.Context, s string) (auth.Credential, error) {
+			return cred, nil
+		}
+	}
+	return client
+}
+
+func newOCIRepository(id string, opts OCIOpts) (*remote.Repository, error) {
+	repo, err := remote.NewRepository(join(opts.Reference, id))
+	if err != nil {
+		return nil, err
+	}
+	repo.PlainHTTP = opts.BasicHTTP
+	repo.Client = getAuthClient(opts)
+	return repo, nil
+}
+
+// join cleans and joins the elements with slash. We avoid filepath.Join, since
+// it uses backslashes on Windows.
+func join(elem ...string) string {
+	for i, e := range elem {
+		if e != "" {
+			return filepath.Clean(strings.Join(elem[i:], "/"))
+		}
+	}
+	return ""
+}
+
+func getDataFromDescriptor(ctx context.Context, store oras.Target, desc ocispec.Descriptor) ([]byte, error) {
+	rc, err := store.Fetch(ctx, desc)
+	if err != nil {
+		return nil, err
+	}
+	data, err := io.ReadAll(rc)
+	if err != nil {
+		return nil, err
+	}
+	err = rc.Close()
+	if err != nil {
+		return nil, err
+	}
+	return data, nil
+}
+
+func checkIDAnnotation(desc ocispec.Descriptor, id string) error {
+	if len(desc.Annotations) != 1 {
+		return fmt.Errorf("expecting 1 Annotation in layer descriptor. Found %d", len(desc.Annotations))
+	}
+	idFound, ok := desc.Annotations["id"]
+	if !ok || idFound != id {
+		return fmt.Errorf("could not find expected id in Descriptor's annotations")
+	}
+	return nil
+}
+
+func (o *OCIWrapper) pushFile(ctx context.Context, opts OCIOpts, reader io.Reader, desc ocispec.Descriptor, id string) error {
+	s := o.oci.NewStore()
+	err := s.Push(ctx, desc, reader)
+	if err != nil {
+		return err
+	}
+
+	fileDescriptors := make([]ocispec.Descriptor, 0, 1)
+	fileDescriptors = append(fileDescriptors, desc)
+	ociOpts := oras.PackManifestOptions{
+		Layers: fileDescriptors,
+	}
+	manifestDescriptor, err := o.oci.PackManifest(ctx, s, oras.PackManifestVersion1_1, artifactType, ociOpts)
+	if err != nil {
+		return err
+	}
+	tag := "latest"
+	err = s.Tag(ctx, manifestDescriptor, tag)
+	if err != nil {
+		return err
+	}
+	repo, err := newOCIRepository(id, opts)
+	if err != nil {
+		return err
+	}
+
+	_, err = o.oci.Copy(ctx, s, tag, repo, tag, oras.DefaultCopyOptions)
+	return err
+}
+
+func (o *OCIWrapper) pullFile(ctx context.Context, opts OCIOpts, id string) ([]byte, error) {
+	s := o.oci.NewStore()
+
+	// copy from remote OCI registry to local memory store
+	repo, err := newOCIRepository(id, opts)
+	if err != nil {
+		return nil, err
+	}
+	tag := "latest"
+	_, err = o.oci.Copy(ctx, repo, tag, s, tag, oras.DefaultCopyOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	// access the root node
+	rootDesc, err := s.Resolve(ctx, tag)
+	if err != nil {
+		return nil, err
+	}
+
+	// fetch the root node of the manifest
+	rootData, err := getDataFromDescriptor(ctx, s, rootDesc)
+	if err != nil {
+		return nil, err
+	}
+
+	// unmarshall the root node in order to access the layers
+	var root struct {
+		MediaType string `json:"mediaType"`
+		Layers    []ocispec.Descriptor
+	}
+	if err := json.Unmarshal(rootData, &root); err != nil {
+		return nil, err
+	}
+	if len(root.Layers) != 1 {
+		return nil, fmt.Errorf("expected 1 layer in OCI manifest, %d found", len(root.Layers))
+	}
+	// get the layer descriptor and fetch from the store
+	desc := root.Layers[0]
+	// when pushing we add the id of the manifest to the annotations
+	// it should match
+	if err := checkIDAnnotation(desc, id); err != nil {
+		return nil, err
+	}
+
+	// return the data for the layer (which is the original fleet manifest)
+	return getDataFromDescriptor(ctx, s, desc)
+}
+
+// PushManifest creates and pushes an OCI manifest to a remote OCI registry with the
+// contents of the given fleet manifest.
+// The OCI manifest will be named after the given id.
+func (o *OCIWrapper) PushManifest(ctx context.Context, opts OCIOpts, id string, m *manifest.Manifest) error {
+	data, err := m.Content()
+	if err != nil {
+		return err
+	}
+	desc := ocispec.Descriptor{
+		MediaType: fileType,
+		Digest:    digest.FromBytes(data),
+		Size:      int64(len(data)),
+		Annotations: map[string]string{
+			"id": id,
+		},
+	}
+	return o.pushFile(ctx, opts, bytes.NewReader(data), desc, id)
+}
+
+// PullManifest pulls the OCI manifest identified by the given id from a remote OCI registry
+// and fills and returns a fleet manifest with the contents.
+func (o *OCIWrapper) PullManifest(ctx context.Context, opts OCIOpts, id string) (*manifest.Manifest, error) {
+	data, err := o.pullFile(ctx, opts, id)
+	if err != nil {
+		return nil, err
+	}
+
+	return manifest.FromJSON(data, "")
+}
+
+// ExperimentalOCIIsEnabled returns true if the EXPERIMENTAL_OCI_STORAGE env variable is set to true
+// returns false otherwise
+func ExperimentalOCIIsEnabled() bool {
+	value, err := strconv.ParseBool(os.Getenv("EXPERIMENTAL_OCI_STORAGE"))
+	return err == nil && value
+}
diff --git a/package/Dockerfile b/package/Dockerfile
index dc0306f7..317aa9a6 100644
--- a/package/Dockerfile
+++ b/package/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILD_ENV=dapper
 ARG ARCH
 
-FROM --platform=linux/$ARCH registry.suse.com/bci/bci-base:15.5 AS base
+FROM --platform=linux/$ARCH registry.suse.com/bci/bci-base:15.6 AS base
 COPY package/log.sh /usr/bin/
 RUN zypper rm -y container-suseconnect && \
     zypper ar --priority=500 https://download.opensuse.org/repositories/Virtualization:containers/5.5/Virtualization:containers.repo && \
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundle_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundle_types.go
index af3c248d..565178cd 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundle_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundle_types.go
@@ -110,6 +110,10 @@ type BundleSpec struct {
 	// DependsOn refers to the bundles which must be ready before this bundle can be deployed.
 	// +nullable
 	DependsOn []BundleRef `json:"dependsOn,omitempty"`
+
+	// ContentsID stores the contents id when deploying contents using an OCI registry.
+	// +nullable
+	ContentsID string `json:"contentsId,omitempty"`
 }
 
 type BundleRef struct {
@@ -335,6 +339,9 @@ type BundleStatus struct {
 	// actual list of resources on a cluster might differ, depending on the
 	// helm chart, value templating, etc..
 	ResourceKey []ResourceKey `json:"resourceKey,omitempty"`
+	// OCIReference is the OCI reference used to store contents, this is
+	// only for informational purposes.
+	OCIReference string `json:"ociReference,omitempty"`
 	// ObservedGeneration is the current generation of the bundle.
 	// +optional
 	ObservedGeneration int64 `json:"observedGeneration"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
index 58b2cd1b..0d6e46fc 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
@@ -313,6 +313,8 @@ type BundleDeploymentSpec struct {
 	DependsOn []BundleRef `json:"dependsOn,omitempty"`
 	// CorrectDrift specifies how drift correction should work.
 	CorrectDrift *CorrectDrift `json:"correctDrift,omitempty"`
+	// OCIContents is true when this deployment's contents is stored in an oci registry
+	OCIContents bool `json:"ociContents,omitempty"`
 }
 
 // BundleDeploymentResource contains the metadata of a deployed resource.
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
index a54a15b6..55a54240 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
@@ -135,6 +135,9 @@ type GitRepoSpec struct {
 
 	// Disables git polling. When enabled only webhooks will be used.
 	DisablePolling bool `json:"disablePolling,omitempty"`
+
+	// OCIRegistry specifies the OCI registry related parameters
+	OCIRegistry *OCIRegistrySpec `json:"ociRegistry,omitempty"`
 }
 
 // GitTarget is a cluster or cluster group to deploy to.
@@ -306,3 +309,22 @@ type CorrectDrift struct {
 	// KeepFailHistory keeps track of failed rollbacks in the helm history.
 	KeepFailHistory bool `json:"keepFailHistory,omitempty"`
 }
+
+type OCIRegistrySpec struct {
+	// Reference of the OCI Registry
+	Reference string `json:"reference,omitempty"`
+
+	// AuthSecretName contains the auth secret where the OCI regristry credentials are stored.
+	// +nullable
+	AuthSecretName string `json:"authSecretName,omitempty"`
+
+	// BasicHTTP uses HTTP connections to the OCI registry when enabled.
+	// +optional
+	// +nullable
+	BasicHTTP bool `json:"basicHTTP,omitempty"`
+
+	// InsecureSkipTLS allows connections to OCI registry without certs when enabled.
+	// +optional
+	// +nullable
+	InsecureSkipTLS bool `json:"insecureSkipTLS,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 d8be5ab8..83e3743f 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -1511,6 +1511,11 @@ func (in *GitRepoSpec) DeepCopyInto(out *GitRepoSpec) {
 		*out = new(CorrectDrift)
 		**out = **in
 	}
+	if in.OCIRegistry != nil {
+		in, out := &in.OCIRegistry, &out.OCIRegistry
+		*out = new(OCIRegistrySpec)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepoSpec.
@@ -1874,6 +1879,21 @@ func (in *NonReadyStatus) DeepCopy() *NonReadyStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OCIRegistrySpec) DeepCopyInto(out *OCIRegistrySpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRegistrySpec.
+func (in *OCIRegistrySpec) DeepCopy() *OCIRegistrySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(OCIRegistrySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Operation) DeepCopyInto(out *Operation) {
 	*out = *in


Test patch:
diff --git a/e2e/assets/single-cluster/test-oci.yaml b/e2e/assets/single-cluster/test-oci.yaml
new file mode 100644
index 00000000..ce25b4dd
--- /dev/null
+++ b/e2e/assets/single-cluster/test-oci.yaml
@@ -0,0 +1,14 @@
+apiVersion: fleet.cattle.io/v1alpha1
+kind: GitRepo
+metadata:
+  name: sample
+  namespace: fleet-local
+spec:
+  repo: "https://github.com/rancher/fleet-test-data"
+  branch: master
+  paths:
+  - simple-chart-oci
+  ociRegistry:
+    reference: {{.OCIReference}}
+    authSecretName: oci-secret
+    insecureSkipTLS: {{.OCIInsecureSkipTLS}}
diff --git a/e2e/drift/drift_test.go b/e2e/drift/drift_test.go
index 72dbf566..83403bec 100644
--- a/e2e/drift/drift_test.go
+++ b/e2e/drift/drift_test.go
@@ -233,7 +233,7 @@ var _ = Describe("Drift", Ordered, func() {
 					"service", "nginx-service",
 					"-o=json",
 					"--type=json",
-					"-p", `[{"op": "replace", "path": "/spec/ports/0/port", "value": 1234}]`,
+					"-p", `[{"op": "replace", "path": "/spec/ports/0/port", "value": 5678}]`,
 				)
 				Expect(err).ToNot(HaveOccurred(), out)
 				GinkgoWriter.Print(out)
diff --git a/e2e/single-cluster/oci_registry_test.go b/e2e/single-cluster/oci_registry_test.go
new file mode 100644
index 00000000..c78a23af
--- /dev/null
+++ b/e2e/single-cluster/oci_registry_test.go
@@ -0,0 +1,419 @@
+package singlecluster_test
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/rancher/fleet/e2e/testenv"
+	"github.com/rancher/fleet/e2e/testenv/kubectl"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+const experimentalEnvVar = "EXPERIMENTAL_OCI_STORAGE"
+
+func getFailedPodNames(k kubectl.Command, namespace string) ([]string, error) {
+	return getPodNamesByFieldSelector(k, namespace, "status.phase=Failed")
+}
+
+func getPodNamesByFieldSelector(k kubectl.Command, namespace, selector string) ([]string, error) {
+	strSelector := fmt.Sprintf("--field-selector=%s", selector)
+	out, err := k.Namespace(namespace).Get("pods", strSelector, "-o", "custom-columns=NAME:metadata.name", "--no-headers")
+	if err != nil {
+		return []string{}, err
+	}
+	return strings.Split(out, "\n"), nil
+}
+
+// getActualEnvVariable returns the value of the given env variable
+// in the gitjob and fleet-controller deployments.
+// In order to be considered correct, both values should match
+func getActualEnvVariable(k kubectl.Command, env string) bool {
+	actualValueController, err := checkEnvVariable(k, "deployment/fleet-controller", env)
+	Expect(err).ToNot(HaveOccurred())
+	actualValueGitjob, err := checkEnvVariable(k, "deployment/gitjob", env)
+	Expect(err).ToNot(HaveOccurred())
+	// both values should be the same, otherwise is a clear symptom that something went wrong
+	Expect(actualValueController).To(Equal(actualValueGitjob))
+	GinkgoWriter.Printf("Actual experimental env variable values: CONTROLLER: %t, GITJOB: %t\n", actualValueController, actualValueGitjob)
+	return actualValueController
+}
+
+// checkEnvVariable runs a kubectl set env --list command for the given component
+// and returns the value of the given env variable
+func checkEnvVariable(k kubectl.Command, component string, env string) (bool, error) {
+	ns := "cattle-fleet-system"
+	out, err := k.Namespace(ns).Run("set", "env", component, "--list")
+	Expect(err).ToNot(HaveOccurred())
+	lines := strings.Split(out, "\n")
+	for _, line := range lines {
+		if strings.Contains(line, env) {
+			keyValue := strings.Split(line, "=")
+			Expect(len(keyValue)).To(Equal(2))
+			return strconv.ParseBool(keyValue[1])
+		}
+	}
+	return false, fmt.Errorf("Environment variable was not found")
+}
+
+// updateExperimentalFlagValue updates the oci storage experimental flag to the given value.
+// It compares the actual value vs the one given and, if they are different, updates the
+// fleet-controller and gitjob deployments to set the given one.
+// It waits until the pods related to both deployments are restarted.
+func updateExperimentalFlagValue(k kubectl.Command, value bool) {
+	actualEnvVal := getActualEnvVariable(k, experimentalEnvVar)
+	GinkgoWriter.Printf("Env variable value to be used in this test: %t\n", value)
+	if actualEnvVal == value {
+		return
+	}
+	ns := "cattle-fleet-system"
+	// get the actual fleet-controller and gitjob pods
+	// When we change the environments variables of the deployment the pods will restart
+	var controllerPod string
+	var err error
+	Eventually(func() string {
+		controllerPod, err = k.Namespace(ns).Get("pod", "-l", "app=fleet-controller", "-l", "fleet.cattle.io/shard-default=true", "-o", "name")
+		Expect(err).ToNot(HaveOccurred())
+		return controllerPod
+	}).WithTimeout(time.Second * 60).Should(ContainSubstring("pod/fleet-controller-"))
+
+	var gitjobPod string
+	Eventually(func() string {
+		gitjobPod, err = k.Namespace(ns).Get("pod", "-l", "app=gitjob", "-o", "name")
+		Expect(err).ToNot(HaveOccurred())
+		return gitjobPod
+	}).WithTimeout(time.Second * 60).Should(ContainSubstring("pod/gitjob-"))
+
+	// set the experimental env value to the deployments
+	strEnvValue := fmt.Sprintf("%s=%t", experimentalEnvVar, value)
+	_, _ = k.Namespace(ns).Run("set", "env", "deployment/fleet-controller", strEnvValue)
+	_, _ = k.Namespace(ns).Run("set", "env", "deployment/gitjob", strEnvValue)
+
+	// wait for both pods to restart
+	Eventually(func() bool {
+		controllerPodNow, _ := k.Namespace(ns).Get("pod", "-l", "app=fleet-controller", "-l", "fleet.cattle.io/shard-default=true", "-o", "name")
+		return controllerPodNow != "" &&
+			strings.Contains(controllerPodNow, "pod/fleet-controller-") &&
+			!strings.Contains(controllerPodNow, controllerPod)
+	}).WithTimeout(time.Second * 30).Should(BeTrue())
+	Eventually(func() bool {
+		gitjobPodNow, _ := k.Namespace(ns).Get("pod", "-l", "app=gitjob", "-o", "name")
+		return gitjobPodNow != "" &&
+			strings.Contains(gitjobPodNow, "pod/gitjob-") &&
+			!strings.Contains(gitjobPodNow, gitjobPod)
+	}).WithTimeout(time.Second * 30).Should(BeTrue())
+}
+
+var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-registry", "infra-setup"), func() {
+	var (
+		asset                  string
+		insecureSkipTLS        bool
+		forceGitRepoURL        string
+		k                      kubectl.Command
+		tempDir                string
+		contentsID             string
+		downstreamNamespace    string
+		ociRegistry            string
+		experimentalFlagBefore bool
+		experimentalValue      bool
+		contentIDToPurge       string
+	)
+
+	BeforeEach(func() {
+		k = env.Kubectl.Namespace(env.Namespace)
+		tempDir = GinkgoT().TempDir()
+		externalIP, err := k.Get("service", "zot-service", "-o", "jsonpath={.status.loadBalancer.ingress[0].ip}")
+		Expect(err).ToNot(HaveOccurred(), externalIP)
+		Expect(net.ParseIP(externalIP)).ShouldNot(BeNil())
+		ociRegistry = fmt.Sprintf("%s:5000", externalIP)
+
+		// store the actual value of the experimental env value
+		// we'll restore in the AfterEach statement if needed
+		experimentalFlagBefore = getActualEnvVariable(k, experimentalEnvVar)
+
+		// reset the value of the contents resource to purge
+		contentIDToPurge = ""
+	})
+
+	JustBeforeEach(func() {
+		updateExperimentalFlagValue(k, experimentalValue)
+		out, err := k.Create(
+			"secret", "generic", "oci-secret",
+			"--from-literal=username="+os.Getenv("CI_OCI_USERNAME"),
+			"--from-literal=password="+os.Getenv("CI_OCI_PASSWORD"),
+		)
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		gitrepo := path.Join(tempDir, "gitrepo.yaml")
+		if forceGitRepoURL != "" {
+			ociRegistry = forceGitRepoURL
+		}
+
+		err = testenv.Template(gitrepo, testenv.AssetPath(asset), struct {
+			OCIReference       string
+			OCIInsecureSkipTLS bool
+		}{
+			ociRegistry,
+			insecureSkipTLS,
+		})
+		Expect(err).ToNot(HaveOccurred())
+
+		out, err = k.Apply("-f", gitrepo)
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		downstreamNamespace, err = k.Namespace("fleet-local").Get("cluster", "local", `-o=jsonpath={.status.namespace}`)
+		Expect(err).ToNot(HaveOccurred(), out)
+	})
+
+	AfterEach(func() {
+		out, err := k.Delete("secret", "oci-secret")
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		gitrepo := path.Join(tempDir, "gitrepo.yaml")
+		out, err = k.Delete("-f", gitrepo)
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		// secrets for bundle and bundledeployment should be gone
+		// secret always begin with "s-"
+		Eventually(func() string {
+			out, _ := k.Namespace("fleet-local").Get("secrets", "-o", "name")
+			return out
+		}).ShouldNot(ContainSubstring("secret/s-"))
+
+		Eventually(func() string {
+			out, _ := k.Namespace(downstreamNamespace).Get("secrets", "-o", "name")
+			return out
+		}).ShouldNot(ContainSubstring("secret/s-"))
+
+		// reset back experimental flag value if needed
+		if experimentalValue != experimentalFlagBefore {
+			GinkgoWriter.Printf("Restoring experimental env variable back to %t n", experimentalFlagBefore)
+			updateExperimentalFlagValue(k, experimentalFlagBefore)
+		}
+
+		// purge content id if needed
+		if contentIDToPurge != "" {
+			out, err := k.Delete("contents", contentIDToPurge)
+			Expect(out).To(ContainSubstring("deleted"))
+			Expect(err).ToNot(HaveOccurred())
+		}
+	})
+
+	When("creating a gitrepo resource with ociRegistry info", func() {
+		Context("containing a valid helm chart", func() {
+			BeforeEach(func() {
+				asset = "single-cluster/test-oci.yaml"
+				insecureSkipTLS = true
+				forceGitRepoURL = ""
+				experimentalValue = true
+			})
+
+			It("deploys the bundle", func() {
+				By("creating the bundle", func() {
+					Eventually(func() string {
+						out, _ := k.Namespace("fleet-local").Get("bundles")
+						return out
+					}).Should(ContainSubstring("sample-simple-chart-oci"))
+				})
+				By("setting the ContentsID field in the bundle", func() {
+					Eventually(func() string {
+						contentsID, _ = k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.spec.contentsId}`)
+						return contentsID
+					}).Should(ContainSubstring("s-"))
+				})
+
+				By("setting the OCI reference status field key to the OCI path of the manifest", func() {
+					Eventually(func() bool {
+						out, _ := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.status.ociReference}`)
+						return out == fmt.Sprintf("oci://%s/%s:latest", ociRegistry, contentsID)
+					}).Should(BeTrue())
+				})
+				By("creating a bundle secret", func() {
+					Eventually(func() string {
+						out, err := k.Namespace("fleet-local").Get("secret", contentsID)
+						if err != nil {
+							// return nothing in case of error
+							// This avoids false positives when kubectl returns "secrets "XXXXX" not found
+							return ""
+						}
+						return out
+					}).Should(ContainSubstring(contentsID))
+				})
+				By("creating a bundledeployment secret", func() {
+					Eventually(func() string {
+						out, err := k.Namespace(downstreamNamespace).Get("secret", contentsID)
+						if err != nil {
+							// return nothing in case of error
+							// This avoids false positives when kubectl returns "secrets "XXXXX" not found
+							return ""
+						}
+						GinkgoWriter.Printf("BundleDeployment secret: %s\n", out)
+						return out
+					}).Should(ContainSubstring(contentsID))
+				})
+				By("not creating a contents resource for this chart", func() {
+					out, _ := k.Get("contents")
+					Expect(out).NotTo(ContainSubstring(contentsID))
+				})
+				By("deploying the helm chart", func() {
+					Eventually(func() string {
+						out, _ := k.Namespace("fleet-local").Get("configmaps")
+						return out
+					}).Should(ContainSubstring("sample-config"))
+				})
+			})
+		})
+	})
+
+	When("creating a gitrepo resource with invalid ociRegistry info and experimental flag is false", func() {
+		Context("containing a public oci based helm chart", func() {
+			BeforeEach(func() {
+				asset = "single-cluster/test-oci.yaml"
+				insecureSkipTLS = true
+				forceGitRepoURL = "not-valid-oci-registry.com"
+				experimentalValue = false
+			})
+
+			It("creates the bundle with no OCI storage", func() {
+				Eventually(func() bool {
+					// check for the bundle
+					out, _ := k.Namespace("fleet-local").Get("bundles")
+					if !strings.Contains(out, "sample-simple-chart-oci") {
+						return false
+					}
+
+					// check for contentsID
+					contentsID, err := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.spec.contentsId}`)
+					Expect(err).ToNot(HaveOccurred())
+					if contentsID != "" {
+						return false
+					}
+					GinkgoWriter.Printf("ContentsID: %s\n", contentsID)
+
+					// bundle secret should not be created
+					secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
+					if strings.Contains(secrets, "s-") {
+						return false
+					}
+
+					// gets the bundles sha256 for the resources
+					// We'll use that to identify the contents resource for this bundle
+					resourcesSha256, _ := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.status.resourcesSha256Sum}`)
+					if resourcesSha256 == "" {
+						return false
+					}
+					// delete the last 3 chars from the sha256 so it matches the contents ID
+					resourcesSha256 = resourcesSha256[:len(resourcesSha256)-3]
+
+					// check that it created a content resource with the above sha256
+					contents, _ := k.Get("contents")
+					if !strings.Contains(contents, resourcesSha256) {
+						return false
+					}
+
+					// save the contents id to purge after the test
+					// this will prevent interference with the previous test
+					// as the resources sha256 is the same
+					contentIDToPurge = "s-" + resourcesSha256
+
+					// finally check that the helm chart was deployed
+					configmaps, _ := k.Namespace("fleet-local").Get("configmaps")
+					return strings.Contains(configmaps, "sample-config")
+				}).Should(BeTrue())
+			})
+		})
+	})
+
+	When("creating a gitrepo resource with invalid ociRegistry info", func() {
+		Context("containing a public oci based helm chart", func() {
+			BeforeEach(func() {
+				asset = "single-cluster/test-oci.yaml"
+				insecureSkipTLS = true
+				forceGitRepoURL = "not-valid-oci-registry.com"
+				experimentalValue = true
+			})
+
+			It("does not create the bundle", func() {
+				// look for failed pods with the name sample-xxxx-xxxx
+				r, _ := regexp.Compile("sample-([a-z0-9]+)-([a-z0-9]+)")
+				var failedJob string
+				Eventually(func() bool {
+					failedPods, err := getFailedPodNames(k, "fleet-local")
+					Expect(err).ToNot(HaveOccurred())
+					for _, pod := range failedPods {
+						if r.MatchString(pod) {
+							failedJob = pod
+							return true
+						}
+					}
+					return false
+				}).Should(BeTrue())
+
+				Expect(failedJob).ShouldNot(BeEmpty())
+				// check that the logs of the job reflect the bad OCI registry
+				logs, err := k.Namespace("fleet-local").Logs(failedJob)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(logs).Should(ContainSubstring("no such host"))
+
+				// we don't fallback to content resources, so the bundle should not be created
+				// at this point
+				bundles, _ := k.Namespace("fleet-local").Get("bundles")
+				Expect(bundles).ShouldNot(ContainSubstring("sample-simple-chart"))
+
+				// bundle secret should not be created either
+				secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
+				Expect(secrets).ShouldNot(ContainSubstring("s-"))
+			})
+		})
+	})
+
+	When("creating a gitrepo resource with valid ociRegistry but not ignoring certs", func() {
+		Context("containing a public oci based helm chart", func() {
+			BeforeEach(func() {
+				asset = "single-cluster/test-oci.yaml"
+				insecureSkipTLS = false
+				forceGitRepoURL = ""
+				experimentalValue = true
+			})
+
+			It("does not create the bundle", func() {
+				// look for failed pods with the name sample-xxxx-xxxx
+				r, _ := regexp.Compile("sample-([a-z0-9]+)-([a-z0-9]+)")
+				var failedJob string
+				Eventually(func() bool {
+					failedPods, err := getFailedPodNames(k, "fleet-local")
+					Expect(err).ToNot(HaveOccurred())
+					for _, pod := range failedPods {
+						if r.MatchString(pod) {
+							failedJob = pod
+							return true
+						}
+					}
+					return false
+				}).Should(BeTrue())
+
+				Expect(failedJob).ShouldNot(BeEmpty())
+				// check that the logs of the job reflect the bad OCI registry
+				logs, err := k.Namespace("fleet-local").Logs(failedJob)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(logs).Should(ContainSubstring("tls: failed to verify certificate: x509"))
+
+				// we don't fallback to content resources, so the bundle should not be created
+				// at this point
+				bundles, _ := k.Namespace("fleet-local").Get("bundles")
+				Expect(bundles).ShouldNot(ContainSubstring("sample-simple-chart"))
+
+				// bundle secret should not be created either
+				secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
+				Expect(secrets).ShouldNot(ContainSubstring("s-"))
+			})
+		})
+	})
+})
diff --git a/internal/cmd/cli/gitcloner/cloner_test.go b/internal/cmd/cli/gitcloner/cloner_test.go
index 8ca0e1ae..a50505fa 100644
--- a/internal/cmd/cli/gitcloner/cloner_test.go
+++ b/internal/cmd/cli/gitcloner/cloner_test.go
@@ -83,9 +83,10 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 				Branch: "master",
 			},
 			expectedCloneOpts: &git.CloneOptions{
-				URL:           "repo",
-				SingleBranch:  true,
-				ReferenceName: "master",
+				URL:               "repo",
+				SingleBranch:      true,
+				ReferenceName:     "master",
+				RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 			},
 		},
 		"branch basic auth": {
@@ -104,6 +105,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 					Username: "user",
 					Password: passwordFileContent,
 				},
+				RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 			},
 		},
 		"branch ssh auth": {
@@ -114,10 +116,11 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 				SSHPrivateKeyFile: sshPrivateKeyFile,
 			},
 			expectedCloneOpts: &git.CloneOptions{
-				URL:           "ssh://git@localhost/test/test-repo",
-				SingleBranch:  true,
-				ReferenceName: "master",
-				Auth:          sshAuth,
+				URL:               "ssh://git@localhost/test/test-repo",
+				SingleBranch:      true,
+				ReferenceName:     "master",
+				Auth:              sshAuth,
+				RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
 			},
 		},
 		"password file does not exist": {
diff --git a/internal/ociwrapper/ociwrapper_test.go b/internal/ociwrapper/ociwrapper_test.go
new file mode 100644
index 00000000..a3f3752e
--- /dev/null
+++ b/internal/ociwrapper/ociwrapper_test.go
@@ -0,0 +1,371 @@
+package ociwrapper
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"os"
+
+	"github.com/golang/mock/gomock"
+	"github.com/opencontainers/go-digest"
+
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"oras.land/oras-go/v2"
+	"oras.land/oras-go/v2/content"
+	"oras.land/oras-go/v2/registry/remote/auth"
+	"oras.land/oras-go/v2/registry/remote/retry"
+
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/rancher/fleet/internal/manifest"
+	"github.com/rancher/fleet/internal/mocks"
+	orasmemory "oras.land/oras-go/v2/content/memory"
+)
+
+type MockOrasOperator struct {
+	ReturnOrasStore  bool
+	Target           *mocks.MockTarget
+	CopyMock         func(ctx context.Context, src oras.ReadOnlyTarget, srcRef string, dst oras.Target, dstRef string, opts oras.CopyOptions) (ocispec.Descriptor, error)
+	PackManifestMock func(ctx context.Context, pusher content.Pusher, packManifestVersion oras.PackManifestVersion, artifactType string, opts oras.PackManifestOptions) (ocispec.Descriptor, error)
+}
+
+func (m *MockOrasOperator) PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion oras.PackManifestVersion, artifactType string, opts oras.PackManifestOptions) (ocispec.Descriptor, error) {
+	if m.PackManifestMock != nil {
+		return m.PackManifestMock(ctx, pusher, packManifestVersion, artifactType, opts)
+	}
+	return oras.PackManifest(ctx, pusher, packManifestVersion, artifactType, opts)
+}
+
+func (m *MockOrasOperator) Copy(ctx context.Context, src oras.ReadOnlyTarget, srcRef string, dst oras.Target, dstRef string, opts oras.CopyOptions) (ocispec.Descriptor, error) {
+	if m.CopyMock != nil {
+		return m.CopyMock(ctx, src, srcRef, dst, dstRef, opts)
+	}
+	return oras.Copy(ctx, src, srcRef, dst, dstRef, opts)
+}
+
+func (m *MockOrasOperator) NewStore() oras.Target {
+	if m.ReturnOrasStore {
+		return orasmemory.New()
+	}
+	return m.Target
+}
+
+func NewMockOrasOperator(ctrl *gomock.Controller, returnOrasStore bool) *MockOrasOperator {
+	return &MockOrasOperator{
+		ReturnOrasStore: returnOrasStore,
+		Target:          mocks.NewMockTarget(ctrl),
+	}
+}
+
+var _ = Describe("OCIUtils tests", func() {
+	var (
+		ctrl *gomock.Controller
+	)
+
+	BeforeEach(func() {
+		ctrl = gomock.NewController(GinkgoT())
+	})
+
+	It("returns an error when can't push to the store", func() {
+		orasOperatorMock := NewMockOrasOperator(ctrl, false)
+		orasOperatorMock.Target.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("TEST ERROR")).Times(1)
+
+		opts := OCIOpts{
+			Reference: "test.com",
+		}
+		manifest := &manifest.Manifest{
+			Commit: "123456",
+			Resources: []fleet.BundleResource{
+				{
+					Name:     "resource1",
+					Content:  "Content1",
+					Encoding: "encoding1",
+				},
+			},
+		}
+		oci := &OCIWrapper{
+			oci: orasOperatorMock,
+		}
+		err := oci.PushManifest(context.Background(), opts, "123", manifest)
+		Expect(err.Error()).To(Equal("TEST ERROR"))
+	})
+	It("returns an error if oras fails packing the manifest", func() {
+		orasOperatorMock := NewMockOrasOperator(ctrl, true)
+		orasOperatorMock.PackManifestMock = func(_ context.Context,
+			_ content.Pusher,
+			_ oras.PackManifestVersion,
+			_ string,
+			_ oras.PackManifestOptions) (ocispec.Descriptor, error) {
+			return ocispec.Descriptor{}, fmt.Errorf("ERROR PACKING")
+		}
+		oci := &OCIWrapper{
+			oci: orasOperatorMock,
+		}
+		opts := OCIOpts{
+			Reference: "test.com",
+		}
+		manifest := &manifest.Manifest{
+			Commit: "123456",
+			Resources: []fleet.BundleResource{
+				{
+					Name:     "resource1",
+					Content:  "Content1",
+					Encoding: "encoding1",
+				},
+			},
+		}
+		err := oci.PushManifest(context.Background(), opts, "123", manifest)
+		Expect(err.Error()).To(Equal("ERROR PACKING"))
+	})
+	It("returns an error when is unable to connect to the registry", func() {
+		oci := NewOCIWrapper()
+		opts := OCIOpts{
+			Reference: "127.0.0.0:2334",
+		}
+		manifest := &manifest.Manifest{
+			Commit: "123456",
+			Resources: []fleet.BundleResource{
+				{
+					Name:     "resource1",
+					Content:  "Content1",
+					Encoding: "encoding1",
+				},
+			},
+		}
+		err := oci.PushManifest(context.Background(), opts, "123", manifest)
+		Expect(err.Error()).To(ContainSubstring("connection refused"))
+	})
+	It("returns an OCI repository with the expected values when using basic HTTP", func() {
+		opts := OCIOpts{
+			Reference: "test.com",
+			BasicHTTP: true,
+		}
+		repo, err := newOCIRepository("1234", opts)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(repo.PlainHTTP).To(Equal(true))
+
+		opts.BasicHTTP = false
+		repo, err = newOCIRepository("1234", opts)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(repo.PlainHTTP).To(Equal(false))
+	})
+	It("return the expected tls client", func() {
+		client := getHTTPClient(true)
+		expected := &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			},
+		}
+		Expect(client).To(Equal(expected))
+
+		client = getHTTPClient(false)
+		Expect(client).To(Equal(retry.DefaultClient))
+	})
+	It("return the expected credentials", func() {
+		opts := OCIOpts{
+			Reference: "test.com",
+			BasicHTTP: true,
+		}
+		client := getAuthClient(opts)
+		Expect(client.Credential).To(BeNil())
+
+		opts.Username = "user"
+		client = getAuthClient(opts)
+		Expect(client.Credential).ToNot(BeNil())
+		cred, err := client.Credential(context.Background(), "test")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(cred).To(Equal(auth.Credential{
+			Username: "user",
+			Password: "",
+		}))
+
+		opts.Password = "pass"
+		client = getAuthClient(opts)
+		Expect(client.Credential).ToNot(BeNil())
+		cred, err = client.Credential(context.Background(), "test")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(cred).To(Equal(auth.Credential{
+			Username: "user",
+			Password: "pass",
+		}))
+	})
+	It("returns an error when using an empty OCI registry reference", func() {
+		opts := OCIOpts{
+			Reference: "",
+		}
+		manifest := &manifest.Manifest{
+			Commit: "123456",
+			Resources: []fleet.BundleResource{
+				{
+					Name:     "resource1",
+					Content:  "Content1",
+					Encoding: "encoding1",
+				},
+			},
+		}
+		oci := NewOCIWrapper()
+		err := oci.PushManifest(context.Background(), opts, "123", manifest)
+		Expect(err.Error()).To(Equal("invalid reference: missing registry or repository"))
+	})
+	It("returns an error if the OCI manifest does not have the expected annotation id", func() {
+		orasOperatorMock := NewMockOrasOperator(ctrl, true)
+		orasOperatorMock.CopyMock = func(_ context.Context,
+			_ oras.ReadOnlyTarget,
+			_ string,
+			target oras.Target,
+			_ string,
+			opts oras.CopyOptions) (ocispec.Descriptor, error) {
+			// fill the store with data
+			data := []byte("This is test")
+			desc := ocispec.Descriptor{
+				MediaType: "application/octet-stream",
+				Digest:    digest.FromBytes(data),
+				Size:      int64(len(data)),
+				Annotations: map[string]string{
+					"id": "example_id", // this is the annotation id that is not the expected one
+				},
+			}
+
+			ctx := context.Background()
+			err := target.Push(ctx, desc, bytes.NewReader(data))
+			Expect(err).ToNot(HaveOccurred())
+
+			fileDescriptors := make([]ocispec.Descriptor, 0, 1)
+			fileDescriptors = append(fileDescriptors, desc)
+			ociOpts := oras.PackManifestOptions{
+				Layers: fileDescriptors,
+			}
+
+			manifestDescriptor, err := oras.PackManifest(ctx, target, oras.PackManifestVersion1_1, artifactType, ociOpts)
+			Expect(err).ToNot(HaveOccurred())
+
+			tag := "latest"
+			err = target.Tag(ctx, manifestDescriptor, tag)
+			Expect(err).ToNot(HaveOccurred())
+
+			return ocispec.Descriptor{}, nil
+		}
+
+		opts := OCIOpts{
+			Reference: "test.com",
+		}
+		oci := &OCIWrapper{
+			oci: orasOperatorMock,
+		}
+		_, err := oci.PullManifest(context.Background(), opts, "s-123456")
+		// s-123456 != example_id so PullManifest should return an error
+		Expect(err.Error()).To(Equal("could not find expected id in Descriptor's annotations"))
+	})
+	It("returns an error if the OCI manifest does not have the expected tag", func() {
+		orasOperatorMock := NewMockOrasOperator(ctrl, true)
+		orasOperatorMock.CopyMock = func(_ context.Context, _ oras.ReadOnlyTarget, _ string, target oras.Target, _ string, opts oras.CopyOptions) (ocispec.Descriptor, error) {
+			// fill the store with data
+			data := []byte("This is test")
+			desc := ocispec.Descriptor{
+				MediaType: "application/octet-stream",
+				Digest:    digest.FromBytes(data),
+				Size:      int64(len(data)),
+				Annotations: map[string]string{
+					"id": "example_id", // this is the annotation id that is not the expected one
+				},
+			}
+
+			ctx := context.Background()
+			err := target.Push(ctx, desc, bytes.NewReader(data))
+			Expect(err).ToNot(HaveOccurred())
+
+			fileDescriptors := make([]ocispec.Descriptor, 0, 1)
+			fileDescriptors = append(fileDescriptors, desc)
+			ociOpts := oras.PackManifestOptions{
+				Layers: fileDescriptors,
+			}
+			manifestDescriptor, err := oras.PackManifest(ctx, target, oras.PackManifestVersion1_1, artifactType, ociOpts)
+			Expect(err).ToNot(HaveOccurred())
+
+			tag := "this_tag_is_not_expected"
+			err = target.Tag(ctx, manifestDescriptor, tag)
+			Expect(err).ToNot(HaveOccurred())
+
+			return ocispec.Descriptor{}, nil
+		}
+
+		opts := OCIOpts{
+			Reference: "test.com",
+		}
+		oci := &OCIWrapper{
+			oci: orasOperatorMock,
+		}
+		_, err := oci.PullManifest(context.Background(), opts, "s-123456")
+		Expect(err.Error()).To(Equal("not found"))
+	})
+	It("returns an error if the OCI manifest is empty", func() {
+		orasOperatorMock := NewMockOrasOperator(ctrl, true)
+		orasOperatorMock.CopyMock = func(_ context.Context, _ oras.ReadOnlyTarget, _ string, target oras.Target, _ string, opts oras.CopyOptions) (ocispec.Descriptor, error) {
+			// fill the store with an empty manifest
+			ctx := context.Background()
+
+			fileDescriptors := make([]ocispec.Descriptor, 0)
+			ociOpts := oras.PackManifestOptions{
+				Layers: fileDescriptors,
+			}
+			manifestDescriptor, err := oras.PackManifest(ctx, target, oras.PackManifestVersion1_1, artifactType, ociOpts)
+			Expect(err).ToNot(HaveOccurred())
+
+			tag := "latest"
+			err = target.Tag(ctx, manifestDescriptor, tag)
+			Expect(err).ToNot(HaveOccurred())
+
+			return ocispec.Descriptor{}, nil
+		}
+
+		opts := OCIOpts{
+			Reference: "test.com",
+		}
+		oci := &OCIWrapper{
+			oci: orasOperatorMock,
+		}
+		_, err := oci.PullManifest(context.Background(), opts, "s-123456")
+		Expect(err.Error()).To(Equal("expecting 1 Annotation in layer descriptor. Found 0"))
+	})
+})
+
+var _ = Describe("OCIUtils Experimental flag tests", func() {
+	var envBeforeTest string
+	const experimentalEnv = "EXPERIMENTAL_OCI_STORAGE"
+
+	BeforeEach(func() {
+		envBeforeTest = os.Getenv(experimentalEnv)
+	})
+
+	AfterEach(func() {
+		if envBeforeTest != "" {
+			// set the value it had before the test
+			Expect(os.Setenv(experimentalEnv, envBeforeTest)).ToNot(HaveOccurred())
+		} else {
+			Expect(os.Unsetenv(experimentalEnv)).ToNot(HaveOccurred())
+		}
+	})
+
+	DescribeTable("Check value returned is the expected one",
+		func(value string, expected bool) {
+			if value == "unset" {
+				Expect(os.Unsetenv(experimentalEnv)).ToNot(HaveOccurred())
+			} else {
+				Expect(os.Setenv(experimentalEnv, value)).ToNot(HaveOccurred())
+			}
+			result := ExperimentalOCIIsEnabled()
+			Expect(result).To(Equal(expected))
+		},
+		Entry("When setting to True", "True", true),
+		Entry("When setting to true", "true", true),
+		Entry("When setting to TRUE", "TRUE", true),
+		Entry("When setting to tRue", "tRue", false),
+		Entry("When setting to false", "false", false),
+		Entry("When setting to whatever", "whatever", false),
+		Entry("When not setting the value", "unset", false),
+	)
+})
diff --git a/internal/ociwrapper/suite_test.go b/internal/ociwrapper/suite_test.go
new file mode 100644
index 00000000..d7c84821
--- /dev/null
+++ b/internal/ociwrapper/suite_test.go
@@ -0,0 +1,22 @@
+package ociwrapper_test
+
+import (
+	"testing"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+const (
+	timeout = 30 * time.Second
+)
+
+func TestFleet(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "OCI Utils Suite")
+}
+
+var _ = BeforeSuite(func() {
+	SetDefaultEventuallyTimeout(timeout)
+})