Repository: crossplane-contrib/provider-helm
Pull request #15 (2020-08-22 03:25:30)

If version is set to "" - pull latest version from the repo and update the spec.
I kept version as required field in the spec to make sure it's explicitly set to "" if we want to pull the latest chart version (vs accidentally forgot to set the version).


Patch:
diff --git a/examples/provider/local-provider.yaml b/examples/provider/local-provider.yaml
index 92924b6..1d0c365 100644
--- a/examples/provider/local-provider.yaml
+++ b/examples/provider/local-provider.yaml
@@ -6,4 +6,4 @@ spec:
   credentialsSecretRef:
     # Get secret name from service account
     name: <helm-provider-token-secret-name>
-    namespace: crossplane-system
\ No newline at end of file
+    namespace: crossplane-system
diff --git a/examples/sample/release.yaml b/examples/sample/release.yaml
index df19bb4..56cd793 100644
--- a/examples/sample/release.yaml
+++ b/examples/sample/release.yaml
@@ -28,4 +28,4 @@ spec:
           optional: false
   providerRef:
     name: local-provider
-  reclaimPolicy: Delete
\ No newline at end of file
+  reclaimPolicy: Delete
diff --git a/pkg/clients/helm/chartdefinition.go b/pkg/clients/helm/chartdefinition.go
deleted file mode 100644
index 5b5e4d9..0000000
--- a/pkg/clients/helm/chartdefinition.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package helm
-
-// ChartDefinition keeps the required information to access a Helm Chart
-type ChartDefinition struct {
-	Repository string
-	Name       string
-	Version    string
-	RepoUser   string
-	RepoPass   string
-}
diff --git a/pkg/clients/helm/client.go b/pkg/clients/helm/client.go
index f3e8b14..3a3519d 100644
--- a/pkg/clients/helm/client.go
+++ b/pkg/clients/helm/client.go
@@ -18,8 +18,10 @@ package helm
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/pkg/errors"
@@ -30,6 +32,8 @@ import (
 	"helm.sh/helm/v3/pkg/release"
 	"k8s.io/client-go/rest"
 	ktype "sigs.k8s.io/kustomize/api/types"
+
+	"github.com/crossplane-contrib/provider-helm/apis/v1alpha1"
 )
 
 const (
@@ -42,15 +46,17 @@ const (
 	errFailedToCheckIfLocalChartExists = "failed to check if cached chart file exists"
 	errFailedToPullChart               = "failed to pull chart"
 	errFailedToLoadChart               = "failed to load chart"
+	errUnexpectedDirContentTmpl        = "expected 1 .tgz chart file, got [%s]"
 )
 
 // Client is the interface to interact with Helm
 type Client interface {
 	GetLastRelease(release string) (*release.Release, error)
-	Install(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
-	Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
+	Install(release string, chart *chart.Chart, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
+	Upgrade(release string, chart *chart.Chart, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
 	Rollback(release string) error
 	Uninstall(release string) error
+	PullAndLoadChart(spec *v1alpha1.ChartSpec, creds *RepoCreds) (*chart.Chart, error)
 }
 
 type client struct {
@@ -76,12 +82,14 @@ func NewClient(log logging.Logger, config *rest.Config, namespace string) (Clien
 	}
 
 	pc := action.NewPull()
+
 	if _, err := os.Stat(chartCache); os.IsNotExist(err) {
 		err = os.Mkdir(chartCache, 0750)
 		if err != nil {
 			return nil, err
 		}
 	}
+
 	pc.DestDir = chartCache
 	pc.Settings = &cli.EnvSettings{}
 
@@ -107,26 +115,87 @@ func NewClient(log logging.Logger, config *rest.Config, namespace string) (Clien
 	}, nil
 }
 
-func (hc *client) pullAndLoadChart(repo, name, version, username, password string) (*chart.Chart, error) {
+func getChartFileName(dir string) (string, error) {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return "", err
+	}
+	if len(files) != 1 {
+		fileNames := make([]string, 0, len(files))
+		for _, f := range files {
+			fileNames = append(fileNames, f.Name())
+		}
+		return "", errors.Errorf(errUnexpectedDirContentTmpl, strings.Join(fileNames, ","))
+	}
+	return files[0].Name(), nil
+}
+
+// Pulls latest chart version. Returns absolute chartFilePath or error.
+func (hc *client) pullLatestChartVersion(spec *v1alpha1.ChartSpec, creds *RepoCreds) (string, error) {
+	tmpDir, err := ioutil.TempDir(chartCache, "")
+	if err != nil {
+		return "", err
+	}
+	defer func() {
+		if err := os.RemoveAll(tmpDir); err != nil {
+			hc.log.WithValues("tmpDir", tmpDir).Info("failed to remove temporary directory")
+		}
+	}()
+
+	if err := hc.pullChart(spec, creds, tmpDir); err != nil {
+		return "", nil
+	}
+
+	chartFileName, err := getChartFileName(tmpDir)
+	if err != nil {
+		return "", err
+	}
+
+	chartFilePath := filepath.Join(chartCache, chartFileName)
+	if err := os.Rename(filepath.Join(tmpDir, chartFileName), chartFilePath); err != nil {
+		return "", nil
+	}
+	return chartFilePath, nil
+}
+
+func (hc *client) pullChart(spec *v1alpha1.ChartSpec, creds *RepoCreds, chartDir string) error {
 	pc := hc.pullClient
 
-	pc.RepoURL = repo
-	pc.Version = version
-	pc.Username = username
-	pc.Password = password
+	pc.RepoURL = spec.Repository
+	pc.Version = spec.Version
+	pc.Username = creds.Username
+	pc.Password = creds.Password
 
-	df := filepath.Join(pc.DestDir, fmt.Sprintf("%s-%s.tgz", name, version))
-	if _, err := os.Stat(df); os.IsNotExist(err) {
-		o, err := pc.Run(name)
-		hc.log.Debug(o)
+	pc.DestDir = chartDir
+
+	o, err := pc.Run(spec.Name)
+	hc.log.Debug(o)
+	if err != nil {
+		return errors.Wrap(err, errFailedToPullChart)
+	}
+	return nil
+}
+
+func (hc *client) PullAndLoadChart(spec *v1alpha1.ChartSpec, creds *RepoCreds) (*chart.Chart, error) {
+	var chartFilePath string
+	var err error
+	if spec.Version == "" {
+		chartFilePath, err = hc.pullLatestChartVersion(spec, creds)
 		if err != nil {
-			return nil, errors.Wrap(err, errFailedToPullChart)
+			return nil, err
+		}
+	} else {
+		chartFilePath = filepath.Join(chartCache, fmt.Sprintf("%s-%s.tgz", spec.Name, spec.Version))
+		if _, err := os.Stat(chartFilePath); os.IsNotExist(err) {
+			if err = hc.pullChart(spec, creds, chartCache); err != nil {
+				return nil, err
+			}
+		} else if err != nil {
+			return nil, errors.Wrap(err, errFailedToCheckIfLocalChartExists)
 		}
-	} else if err != nil {
-		return nil, errors.Wrap(err, errFailedToCheckIfLocalChartExists)
 	}
 
-	chart, err := loader.Load(df)
+	chart, err := loader.Load(chartFilePath)
 	if err != nil {
 		return nil, errors.Wrap(err, errFailedToLoadChart)
 	}
@@ -137,14 +206,9 @@ func (hc *client) GetLastRelease(release string) (*release.Release, error) {
 	return hc.getClient.Run(release)
 }
 
-func (hc *client) Install(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
+func (hc *client) Install(release string, chart *chart.Chart, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
 	hc.installClient.ReleaseName = release
 
-	c, err := hc.pullAndLoadChart(chartDef.Repository, chartDef.Name, chartDef.Version, chartDef.RepoUser, chartDef.RepoPass)
-	if err != nil {
-		return nil, err
-	}
-
 	if len(patches) > 0 {
 		hc.installClient.PostRenderer = &KustomizationRender{
 			patches: patches,
@@ -152,19 +216,14 @@ func (hc *client) Install(release string, chartDef ChartDefinition, vals map[str
 		}
 	}
 
-	return hc.installClient.Run(c, vals)
+	return hc.installClient.Run(chart, vals)
 }
 
-func (hc *client) Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
+func (hc *client) Upgrade(release string, chart *chart.Chart, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
 	// Reset values so that source of truth for desired state is always the CR itself
 	hc.upgradeClient.ResetValues = true
 	hc.upgradeClient.MaxHistory = releaseMaxHistory
 
-	c, err := hc.pullAndLoadChart(chartDef.Repository, chartDef.Name, chartDef.Version, chartDef.RepoUser, chartDef.RepoPass)
-	if err != nil {
-		return nil, err
-	}
-
 	if len(patches) > 0 {
 		hc.upgradeClient.PostRenderer = &KustomizationRender{
 			patches: patches,
@@ -172,7 +231,7 @@ func (hc *client) Upgrade(release string, chartDef ChartDefinition, vals map[str
 		}
 	}
 
-	return hc.upgradeClient.Run(release, c, vals)
+	return hc.upgradeClient.Run(release, chart, vals)
 }
 
 func (hc *client) Rollback(release string) error {
diff --git a/pkg/clients/helm/repocreds.go b/pkg/clients/helm/repocreds.go
new file mode 100644
index 0000000..a36c9bf
--- /dev/null
+++ b/pkg/clients/helm/repocreds.go
@@ -0,0 +1,7 @@
+package helm
+
+// RepoCreds keeps auth information to access a Helm Chart
+type RepoCreds struct {
+	Username string
+	Password string
+}
diff --git a/pkg/controller/chart.go b/pkg/controller/chart.go
index 76a83ad..3c31661 100644
--- a/pkg/controller/chart.go
+++ b/pkg/controller/chart.go
@@ -19,11 +19,11 @@ package controller
 import (
 	"context"
 
+	runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"github.com/crossplane-contrib/provider-helm/apis/v1alpha1"
 	helmClient "github.com/crossplane-contrib/provider-helm/pkg/clients/helm"
 )
 
@@ -39,32 +39,29 @@ const (
 	errChartPullSecretMissingPassword  = "password missing in chart pull secret"
 )
 
-func chartDefFromSpec(ctx context.Context, kube client.Client, spec v1alpha1.ChartSpec) (helmClient.ChartDefinition, error) {
+func repoCredsFromSecret(ctx context.Context, kube client.Client, secretRef runtimev1alpha1.SecretReference) (*helmClient.RepoCreds, error) {
 	repoUser := ""
 	repoPass := ""
-	if spec.PullSecretRef.Name != "" {
-		if spec.PullSecretRef.Namespace == "" {
-			return helmClient.ChartDefinition{}, errors.New(errChartPullSecretMissingNamespace)
+	if secretRef.Name != "" {
+		if secretRef.Namespace == "" {
+			return nil, errors.New(errChartPullSecretMissingNamespace)
 		}
-		d, err := getSecretData(ctx, kube, types.NamespacedName{Name: spec.PullSecretRef.Name, Namespace: spec.PullSecretRef.Namespace})
+		d, err := getSecretData(ctx, kube, types.NamespacedName{Name: secretRef.Name, Namespace: secretRef.Namespace})
 		if err != nil {
-			return helmClient.ChartDefinition{}, errors.Wrap(err, errFailedToGetRepoPullSecret)
+			return nil, errors.Wrap(err, errFailedToGetRepoPullSecret)
 		}
 		repoUser = string(d[keyRepoUsername])
 		if repoUser == "" {
-			return helmClient.ChartDefinition{}, errors.New(errChartPullSecretMissingUsername)
+			return nil, errors.New(errChartPullSecretMissingUsername)
 		}
 		repoPass = string(d[keyRepoPassword])
 		if repoPass == "" {
-			return helmClient.ChartDefinition{}, errors.New(errChartPullSecretMissingPassword)
+			return nil, errors.New(errChartPullSecretMissingPassword)
 		}
 	}
 
-	return helmClient.ChartDefinition{
-		Repository: spec.Repository,
-		Name:       spec.Name,
-		Version:    spec.Version,
-		RepoUser:   repoUser,
-		RepoPass:   repoPass,
+	return &helmClient.RepoCreds{
+		Username: repoUser,
+		Password: repoPass,
 	}, nil
 }
diff --git a/pkg/controller/release.go b/pkg/controller/release.go
index 518eace..70c5098 100644
--- a/pkg/controller/release.go
+++ b/pkg/controller/release.go
@@ -26,11 +26,14 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	kubev1alpha1 "github.com/crossplane/crossplane/apis/kubernetes/v1alpha1"
 	"github.com/pkg/errors"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/storage/driver"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	ktype "sigs.k8s.io/kustomize/api/types"
 
 	"github.com/crossplane-contrib/provider-helm/apis/v1alpha1"
 	"github.com/crossplane-contrib/provider-helm/pkg/clients"
@@ -48,11 +51,12 @@ const (
 	errFailedToInstall            = "failed to install release"
 	errFailedToUpgrade            = "failed to upgrade release"
 	errFailedToUninstall          = "failed to uninstall release"
-	errFailedToBuildChartDef      = "failed to build chart definition"
+	errFailedToGetRepoCreds       = "failed to get user name and password from secret reference"
 	errFailedToComposeValues      = "failed to compose values"
 	errFailedToCreateRestConfig   = "cannot create new rest config using provider secret"
 	errFailedToLoadPatches        = "failed to load patches"
 	errFailedToUpdatePatchSha     = "failed to update patch sha"
+	errFailedToSetVersion         = "failed to update chart spec with the latest version"
 )
 
 // SetupRelease adds a controller that reconciles Release managed resources.
@@ -171,88 +175,73 @@ func (e *helmExternal) Observe(ctx context.Context, mg resource.Managed) (manage
 	}, nil
 }
 
-func (e *helmExternal) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) {
-	cr, ok := mg.(*v1alpha1.Release)
-
-	if !ok {
-		return managed.ExternalCreation{}, errors.New(errNotRelease)
-	}
-
-	e.logger.Debug("Creating")
+type chartAction func(release string, chart *chart.Chart, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
 
+func (e *helmExternal) run(ctx context.Context, cr *v1alpha1.Release, action chartAction) error { // nolint:gocyclo
 	cv, err := composeValuesFromSpec(ctx, e.localKube, cr.Spec.ForProvider.ValuesSpec)
 	if err != nil {
-		return managed.ExternalCreation{}, errors.Wrap(err, errFailedToComposeValues)
+		return errors.Wrap(err, errFailedToComposeValues)
 	}
 
-	cd, err := chartDefFromSpec(ctx, e.localKube, cr.Spec.ForProvider.Chart)
+	creds, err := repoCredsFromSecret(ctx, e.localKube, cr.Spec.ForProvider.Chart.PullSecretRef)
 	if err != nil {
-		return managed.ExternalCreation{}, errors.Wrap(err, errFailedToBuildChartDef)
+		return errors.Wrap(err, errFailedToGetRepoCreds)
 	}
 
 	p, err := e.patch.getFromSpec(ctx, e.localKube, cr.Spec.ForProvider.PatchesFrom)
 	if err != nil {
-		return managed.ExternalCreation{}, errors.Wrap(err, errFailedToLoadPatches)
+		return errors.Wrap(err, errFailedToLoadPatches)
 	}
 
-	rel, err := e.helm.Install(meta.GetExternalName(cr), cd, cv, p)
+	chart, err := e.helm.PullAndLoadChart(&cr.Spec.ForProvider.Chart, creds)
 	if err != nil {
-		return managed.ExternalCreation{}, errors.Wrap(err, errFailedToInstall)
+		return err
+	}
+	if cr.Spec.ForProvider.Chart.Version == "" {
+		cr.Spec.ForProvider.Chart.Version = chart.Metadata.Version
+		if err := e.localKube.Update(ctx, cr); err != nil {
+			return errors.Wrap(err, errFailedToSetVersion)
+		}
+	}
+
+	rel, err := action(meta.GetExternalName(cr), chart, cv, p)
+
+	if err != nil {
+		return err
 	}
 
 	if rel == nil {
-		return managed.ExternalCreation{}, errors.New(errLastReleaseIsNil)
+		return errors.New(errLastReleaseIsNil)
 	}
 
 	sha, err := e.patch.shaOf(p)
 	if err != nil {
-		return managed.ExternalCreation{}, errors.Wrap(err, errFailedToUpdatePatchSha)
+		return errors.Wrap(err, errFailedToUpdatePatchSha)
 	}
 	cr.Status.PatchesSha = sha
 	cr.Status.AtProvider = generateObservation(rel)
 
-	return managed.ExternalCreation{}, nil
+	return nil
 }
 
-func (e *helmExternal) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
+func (e *helmExternal) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) {
 	cr, ok := mg.(*v1alpha1.Release)
 	if !ok {
-		return managed.ExternalUpdate{}, errors.New(errNotRelease)
-	}
-
-	e.logger.Debug("Updating")
-
-	cv, err := composeValuesFromSpec(ctx, e.localKube, cr.Spec.ForProvider.ValuesSpec)
-	if err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errFailedToComposeValues)
-	}
-
-	cd, err := chartDefFromSpec(ctx, e.localKube, cr.Spec.ForProvider.Chart)
-	if err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errFailedToBuildChartDef)
-	}
-
-	p, err := e.patch.getFromSpec(ctx, e.localKube, cr.Spec.ForProvider.PatchesFrom)
-	if err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errFailedToLoadPatches)
+		return managed.ExternalCreation{}, errors.New(errNotRelease)
 	}
 
-	rel, err := e.helm.Upgrade(meta.GetExternalName(cr), cd, cv, p)
-	if err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errFailedToUpgrade)
-	}
-	if rel == nil {
-		return managed.ExternalUpdate{}, errors.New(errLastReleaseIsNil)
-	}
+	e.logger.Debug("Creating")
+	return managed.ExternalCreation{}, errors.Wrap(e.run(ctx, cr, e.helm.Install), errFailedToInstall)
+}
 
-	sha, err := e.patch.shaOf(p)
-	if err != nil {
-		return managed.ExternalUpdate{}, errors.Wrap(err, errFailedToUpdatePatchSha)
+func (e *helmExternal) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
+	cr, ok := mg.(*v1alpha1.Release)
+	if !ok {
+		return managed.ExternalUpdate{}, errors.New(errNotRelease)
 	}
-	cr.Status.PatchesSha = sha
 
-	cr.Status.AtProvider = generateObservation(rel)
-	return managed.ExternalUpdate{}, nil
+	e.logger.Debug("Updating")
+	return managed.ExternalUpdate{}, errors.Wrap(e.run(ctx, cr, e.helm.Upgrade), errFailedToUpgrade)
 }
 
 func (e *helmExternal) Delete(_ context.Context, mg resource.Managed) error {


Test patch:
diff --git a/pkg/controller/chart_test.go b/pkg/controller/chart_test.go
index 55d6daf..183ac81 100644
--- a/pkg/controller/chart_test.go
+++ b/pkg/controller/chart_test.go
@@ -15,12 +15,10 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"github.com/crossplane-contrib/provider-helm/apis/v1alpha1"
 	"github.com/crossplane-contrib/provider-helm/pkg/clients/helm"
 )
 
 const (
-	testRepo                = "testrepo"
 	testChart               = "testchart"
 	testVersion             = "v1"
 	testPullSecretName      = "testcreds"
@@ -33,13 +31,13 @@ var (
 	errBoom = errors.New("boom")
 )
 
-func Test_chartDefFromSpec(t *testing.T) {
+func Test_userInfoFromSecret(t *testing.T) {
 	type args struct {
-		kube client.Client
-		spec v1alpha1.ChartSpec
+		kube      client.Client
+		secretRef runtimev1alpha1.SecretReference
 	}
 	type want struct {
-		out helm.ChartDefinition
+		out *helm.RepoCreds
 		err error
 	}
 	cases := map[string]struct {
@@ -51,18 +49,9 @@ func Test_chartDefFromSpec(t *testing.T) {
 				kube: &test.MockClient{
 					MockGet: nil,
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-				},
 			},
 			want: want{
-				out: helm.ChartDefinition{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-				},
+				out: &helm.RepoCreds{},
 				err: nil,
 			},
 		},
@@ -83,17 +72,11 @@ func Test_chartDefFromSpec(t *testing.T) {
 						return errBoom
 					},
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					PullSecretRef: runtimev1alpha1.SecretReference{
-						Name: testPullSecretName,
-					},
+				secretRef: runtimev1alpha1.SecretReference{
+					Name: testPullSecretName,
 				},
 			},
 			want: want{
-				out: helm.ChartDefinition{},
 				err: errors.New(errChartPullSecretMissingNamespace),
 			},
 		},
@@ -107,18 +90,12 @@ func Test_chartDefFromSpec(t *testing.T) {
 						return errBoom
 					},
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					PullSecretRef: runtimev1alpha1.SecretReference{
-						Name:      testPullSecretName,
-						Namespace: testPullSecretNamespace,
-					},
+				secretRef: runtimev1alpha1.SecretReference{
+					Name:      testPullSecretName,
+					Namespace: testPullSecretNamespace,
 				},
 			},
 			want: want{
-				out: helm.ChartDefinition{},
 				err: errors.Wrap(
 					errors.Wrap(kerrors.NewNotFound(schema.GroupResource{Group: corev1.GroupName}, testPullSecretName),
 						fmt.Sprintf(errFailedToGetSecret, testPullSecretNamespace)), errFailedToGetRepoPullSecret),
@@ -140,18 +117,13 @@ func Test_chartDefFromSpec(t *testing.T) {
 						return errBoom
 					},
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					PullSecretRef: runtimev1alpha1.SecretReference{
-						Name:      testPullSecretName,
-						Namespace: testPullSecretNamespace,
-					},
+				secretRef: runtimev1alpha1.SecretReference{
+					Name:      testPullSecretName,
+					Namespace: testPullSecretNamespace,
 				},
 			},
 			want: want{
-				out: helm.ChartDefinition{},
+				out: nil,
 				err: errors.New(errChartPullSecretMissingUsername),
 			},
 		},
@@ -171,18 +143,12 @@ func Test_chartDefFromSpec(t *testing.T) {
 						return errBoom
 					},
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					PullSecretRef: runtimev1alpha1.SecretReference{
-						Name:      testPullSecretName,
-						Namespace: testPullSecretNamespace,
-					},
+				secretRef: runtimev1alpha1.SecretReference{
+					Name:      testPullSecretName,
+					Namespace: testPullSecretNamespace,
 				},
 			},
 			want: want{
-				out: helm.ChartDefinition{},
 				err: errors.New(errChartPullSecretMissingPassword),
 			},
 		},
@@ -203,23 +169,15 @@ func Test_chartDefFromSpec(t *testing.T) {
 						return errBoom
 					},
 				},
-				spec: v1alpha1.ChartSpec{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					PullSecretRef: runtimev1alpha1.SecretReference{
-						Name:      testPullSecretName,
-						Namespace: testPullSecretNamespace,
-					},
+				secretRef: runtimev1alpha1.SecretReference{
+					Name:      testPullSecretName,
+					Namespace: testPullSecretNamespace,
 				},
 			},
 			want: want{
-				out: helm.ChartDefinition{
-					Repository: testRepo,
-					Name:       testChart,
-					Version:    testVersion,
-					RepoUser:   testUser,
-					RepoPass:   testPass,
+				out: &helm.RepoCreds{
+					Username: testUser,
+					Password: testPass,
 				},
 				err: nil,
 			},
@@ -227,9 +185,9 @@ func Test_chartDefFromSpec(t *testing.T) {
 	}
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
-			got, gotErr := chartDefFromSpec(context.Background(), tc.args.kube, tc.args.spec)
+			got, gotErr := repoCredsFromSecret(context.Background(), tc.args.kube, tc.args.secretRef)
 			if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
-				t.Fatalf("chartDefFromSpec(...): -want error, +got error: %s", diff)
+				t.Fatalf("repoCredsFromSecret(...): -want error, +got error: %s", diff)
 			}
 			if diff := cmp.Diff(tc.want.out, got); diff != "" {
 				t.Errorf("chartDefFromSpec(...): -want result, +got result: %s", diff)
diff --git a/pkg/controller/release_test.go b/pkg/controller/release_test.go
index ef5fd03..280d4b6 100644
--- a/pkg/controller/release_test.go
+++ b/pkg/controller/release_test.go
@@ -52,7 +52,11 @@ func helmRelase(rm ...helmReleaseModifier) *v1alpha1.Release {
 					Name: providerName,
 				},
 			},
-			ForProvider: v1alpha1.ReleaseParameters{},
+			ForProvider: v1alpha1.ReleaseParameters{
+				Chart: v1alpha1.ChartSpec{
+					Version: testVersion,
+				},
+			},
 		},
 		Status: v1alpha1.ReleaseStatus{},
 	}
@@ -65,29 +69,31 @@ func helmRelase(rm ...helmReleaseModifier) *v1alpha1.Release {
 }
 
 type MockGetLastReleaseFn func(release string) (*release.Release, error)
-type MockInstallFn func(release string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (*release.Release, error)
-type MockUpgradeFn func(release string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (*release.Release, error)
+type MockInstallFn func(release string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (*release.Release, error)
+type MockUpgradeFn func(release string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (*release.Release, error)
 type MockRollBackFn func(release string) error
 type MockUninstallFn func(release string) error
+type MockPullAndLoadChartFn func(spec *v1alpha1.ChartSpec, creds *helmClient.RepoCreds) (*chart.Chart, error)
 
 type MockHelmClient struct {
-	MockGetLastRelease MockGetLastReleaseFn
-	MockInstall        MockInstallFn
-	MockUpgrade        MockUpgradeFn
-	MockRollBack       MockRollBackFn
-	MockUninstall      MockUninstallFn
+	MockGetLastRelease   MockGetLastReleaseFn
+	MockInstall          MockInstallFn
+	MockUpgrade          MockUpgradeFn
+	MockRollBack         MockRollBackFn
+	MockUninstall        MockUninstallFn
+	MockPullAndLoadChart MockPullAndLoadChartFn
 }
 
 func (c *MockHelmClient) GetLastRelease(release string) (*release.Release, error) {
 	return c.MockGetLastRelease(release)
 }
 
-func (c *MockHelmClient) Install(release string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (*release.Release, error) {
-	return c.MockInstall(release, chartDef, vals, patches)
+func (c *MockHelmClient) Install(release string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (*release.Release, error) {
+	return c.MockInstall(release, chart, vals, patches)
 }
 
-func (c *MockHelmClient) Upgrade(release string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (*release.Release, error) {
-	return c.MockUpgrade(release, chartDef, vals, patches)
+func (c *MockHelmClient) Upgrade(release string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (*release.Release, error) {
+	return c.MockUpgrade(release, chart, vals, patches)
 }
 
 func (c *MockHelmClient) Rollback(release string) error {
@@ -98,6 +104,13 @@ func (c *MockHelmClient) Uninstall(release string) error {
 	return c.MockUninstall(release)
 }
 
+func (c *MockHelmClient) PullAndLoadChart(spec *v1alpha1.ChartSpec, creds *helmClient.RepoCreds) (*chart.Chart, error) {
+	if c.MockPullAndLoadChart != nil {
+		return c.MockPullAndLoadChart(spec, creds)
+	}
+	return nil, nil
+}
+
 type notHelmRelease struct {
 	resource.Managed
 }
@@ -407,6 +420,7 @@ func Test_helmExternal_Create(t *testing.T) {
 		kube      client.Client
 		helm      helmClient.Client
 		mg        resource.Managed
+		updateFn  func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error
 	}
 	type want struct {
 		err error
@@ -426,7 +440,7 @@ func Test_helmExternal_Create(t *testing.T) {
 		"InstalledFailed": {
 			args: args{
 				helm: &MockHelmClient{
-					MockInstall: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockInstall: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return nil, errBoom
 					},
 				},
@@ -439,20 +453,20 @@ func Test_helmExternal_Create(t *testing.T) {
 		"InstalledButLastReleaseIsNil": {
 			args: args{
 				helm: &MockHelmClient{
-					MockInstall: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockInstall: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return nil, nil
 					},
 				},
 				mg: helmRelase(),
 			},
 			want: want{
-				err: errors.New(errLastReleaseIsNil),
+				err: errors.Wrap(errors.New(errLastReleaseIsNil), errFailedToInstall),
 			},
 		},
 		"Success": {
 			args: args{
 				helm: &MockHelmClient{
-					MockInstall: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockInstall: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return &release.Release{}, nil
 					},
 				},
@@ -462,6 +476,35 @@ func Test_helmExternal_Create(t *testing.T) {
 				err: nil,
 			},
 		},
+		"LatestVersion": {
+			args: args{
+				helm: &MockHelmClient{
+					MockInstall: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+						return &release.Release{}, nil
+					},
+					MockPullAndLoadChart: func(spec *v1alpha1.ChartSpec, creds *helmClient.RepoCreds) (*chart.Chart, error) {
+						return &chart.Chart{
+							Metadata: &chart.Metadata{
+								Version: testVersion,
+							},
+						}, nil
+					},
+				},
+				mg: helmRelase(func(r *v1alpha1.Release) {
+					r.Spec.ForProvider.Chart.Version = ""
+				}),
+				updateFn: func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
+					cr := obj.(*v1alpha1.Release)
+					if diff := cmp.Diff(cr.Spec.ForProvider.Chart.Version, testVersion); diff != "" {
+						t.Fatalf("updateFn(...): -want version, +got version: %s", diff)
+					}
+					return nil
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
 	}
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
@@ -472,6 +515,11 @@ func Test_helmExternal_Create(t *testing.T) {
 				helm:      tc.args.helm,
 				patch:     newPatcher(),
 			}
+			if tc.args.updateFn != nil {
+				e.localKube = &test.MockClient{
+					MockUpdate: tc.args.updateFn,
+				}
+			}
 			_, gotErr := e.Create(context.Background(), tc.args.mg)
 			if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" {
 				t.Fatalf("e.Create(...): -want error, +got error: %s", diff)
@@ -505,7 +553,7 @@ func Test_helmExternal_Update(t *testing.T) {
 		"UpgradeFailed": {
 			args: args{
 				helm: &MockHelmClient{
-					MockUpgrade: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockUpgrade: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return nil, errBoom
 					},
 				},
@@ -518,20 +566,20 @@ func Test_helmExternal_Update(t *testing.T) {
 		"UpgradedButLastReleaseIsNil": {
 			args: args{
 				helm: &MockHelmClient{
-					MockUpgrade: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockUpgrade: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return nil, nil
 					},
 				},
 				mg: helmRelase(),
 			},
 			want: want{
-				err: errors.New(errLastReleaseIsNil),
+				err: errors.Wrap(errors.New(errLastReleaseIsNil), errFailedToUpgrade),
 			},
 		},
 		"Success": {
 			args: args{
 				helm: &MockHelmClient{
-					MockUpgrade: func(r string, chartDef helmClient.ChartDefinition, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
+					MockUpgrade: func(r string, chart *chart.Chart, vals map[string]interface{}, patches []types.Patch) (hr *release.Release, err error) {
 						return &release.Release{}, nil
 					},
 				},