diff --git a/README.md b/README.md index dc8aca585..b26584c7a 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,15 @@ export DAPR_HELM_REPO_PASSWORD="passwd_xxx" Setting the above parameters will allow `dapr init -k` to install Dapr images from the configured Helm repository. +A local Helm repo is also supported, this can either be a directory path or an existing .tgz file. + +export DAPR_HELM_REPO_URL="/home/user/dapr/helm-charts" + +To directly use HEAD helm charts, create two local symlinks under `DAPR_HELM_REPO_URL` that point to the `charts` folder of each repo: + * `dapr-dashboard-latest` -> `https://github.com/dapr/dashboard/tree/master/chart/dapr-dashboard` + * `dapr-latest` -> `https://github.com/dapr/dapr/tree/master/charts/dapr` + + ### Launch Dapr and your app The Dapr CLI lets you debug easily by launching both Dapr and your app. diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index f6bd78e5a..df1079648 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -190,12 +190,15 @@ func getVersion(releaseName string, version string) (string, error) { return actualVersion, nil } -func createTempDir() (string, error) { +func createTempDir() (string, func(), error) { dir, err := os.MkdirTemp("", "dapr") if err != nil { - return "", fmt.Errorf("error creating temp dir: %w", err) + return "", func() {}, fmt.Errorf("error creating temp dir: %w", err) } - return dir, nil + cleanup := func() { + os.RemoveAll(dir) + } + return dir, cleanup, nil } func locateChartFile(dirPath string) (string, error) { @@ -206,7 +209,12 @@ func locateChartFile(dirPath string) (string, error) { return filepath.Join(dirPath, files[0].Name()), nil } -func getHelmChart(version, releaseName, helmRepo string, config *helm.Configuration) (*chart.Chart, error) { +func pullHelmChart(version, releaseName, helmRepo string, config *helm.Configuration) (string, func(), error) { + // is helmRepo already a directory path or a .tgz file? (ie. /home/user/dapr/helm-charts). + if localPath, err := utils.DiscoverHelmPath(helmRepo, releaseName, version); err == nil { + return localPath, func() {}, nil + } + pull := helm.NewPullWithOpts(helm.WithConfig(config)) pull.RepoURL = helmRepo pull.Username = utils.GetEnv("DAPR_HELM_REPO_USERNAME", "") @@ -218,24 +226,39 @@ func getHelmChart(version, releaseName, helmRepo string, config *helm.Configurat pull.Version = chartVersion(version) } - dir, err := createTempDir() + dir, cleanup, err := createTempDir() if err != nil { - return nil, err + return "", nil, fmt.Errorf("unable to create temp dir: %w", err) } - defer os.RemoveAll(dir) pull.DestDir = dir _, err = pull.Run(releaseName) if err != nil { - return nil, err + return "", cleanup, fmt.Errorf("unable to pull chart from repo: %w", err) } chartPath, err := locateChartFile(dir) if err != nil { - return nil, err + return "", cleanup, fmt.Errorf("unable to locate chart: %w", err) } - return loader.Load(chartPath) + + return chartPath, cleanup, nil +} + +func getHelmChart(version, releaseName, helmRepo string, config *helm.Configuration) (*chart.Chart, error) { + chartPath, cleanup, err := pullHelmChart(version, releaseName, helmRepo, config) + defer cleanup() + if err != nil { + return nil, fmt.Errorf("unable to pull helm chart: %w", err) + } + + chart, err := loader.Load(chartPath) + if err != nil { + return nil, fmt.Errorf("unable to load chart from path: %w", err) + } + + return chart, nil } func daprChartValues(config InitConfiguration, version string) (map[string]interface{}, error) { @@ -461,8 +484,8 @@ spec: zipkin: endpointAddress: "http://dapr-dev-zipkin.default.svc.cluster.local:9411/api/v2/spans" ` - tempDirPath, err := createTempDir() - defer os.RemoveAll(tempDirPath) + tempDirPath, cleanup, err := createTempDir() + defer cleanup() if err != nil { return err } diff --git a/pkg/runfileconfig/run_file_config_parser.go b/pkg/runfileconfig/run_file_config_parser.go index 207bfd2fa..03878abdf 100644 --- a/pkg/runfileconfig/run_file_config_parser.go +++ b/pkg/runfileconfig/run_file_config_parser.go @@ -216,7 +216,7 @@ func (a *RunFileConfig) resolvePathToAbsAndValidate(baseDir string, paths ...*st return err } *path = absPath - if err = utils.ValidateFilePath(*path); err != nil { + if err = utils.ValidatePath(*path); err != nil { return err } } @@ -264,7 +264,7 @@ func (a *RunFileConfig) resolveResourcesFilePath(app *App) error { return nil } localResourcesDir := filepath.Join(app.AppDirPath, standalone.DefaultDaprDirName, standalone.DefaultResourcesDirName) - if err := utils.ValidateFilePath(localResourcesDir); err == nil { + if err := utils.ValidatePath(localResourcesDir); err == nil { app.ResourcesPaths = []string{localResourcesDir} } else if len(a.Common.ResourcesPaths) > 0 { app.ResourcesPaths = append(app.ResourcesPaths, a.Common.ResourcesPaths...) @@ -285,7 +285,7 @@ func (a *RunFileConfig) resolveConfigFilePath(app *App) error { return nil } localConfigFile := filepath.Join(app.AppDirPath, standalone.DefaultDaprDirName, standalone.DefaultConfigFileName) - if err := utils.ValidateFilePath(localConfigFile); err == nil { + if err := utils.ValidatePath(localConfigFile); err == nil { app.ConfigFile = localConfigFile } else if len(strings.TrimSpace(a.Common.ConfigFile)) > 0 { app.ConfigFile = a.Common.ConfigFile diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go index b6c1b5b4a..24e8f13c4 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -774,8 +774,10 @@ func installTest(details VersionDetails, opts TestOptions) func(t *testing.T) { args = append(args, "--dev") } if !details.UseDaprLatestVersion { - // TODO: Pass dashboard-version also when charts are released. - args = append(args, "--runtime-version", details.RuntimeVersion) + args = append(args, []string{ + "--runtime-version", details.RuntimeVersion, + "--dashboard-version", details.DashboardVersion, + }...) } if opts.HAEnabled { args = append(args, "--enable-ha") diff --git a/tests/e2e/kubernetes/kubernetes_test.go b/tests/e2e/kubernetes/kubernetes_test.go index d080eba4c..91ea8a056 100644 --- a/tests/e2e/kubernetes/kubernetes_test.go +++ b/tests/e2e/kubernetes/kubernetes_test.go @@ -17,9 +17,17 @@ limitations under the License. package kubernetes_test import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "path/filepath" + "strings" "testing" "github.com/dapr/cli/tests/e2e/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestKubernetesNonHAModeMTLSDisabled(t *testing.T) { @@ -500,3 +508,126 @@ func TestK8sInstallwithoutRuntimeVersionwithMarinerImagesFlag(t *testing.T) { t.Run(tc.Name, tc.Callable) } } + +func TestKubernetesLocalFileHelmRepoInstall(t *testing.T) { + // ensure clean env for test + ensureCleanEnv(t, false) + + // create a temp dir to store the helm repo + helmRepoPath, err := os.MkdirTemp("", "dapr-e2e-kube-with-env-*") + assert.NoError(t, err) + // defer os.RemoveAll(helmRepoPath) // clean up + + // copy all .tar.gz files from testdata dir and uncompress them + copyAndUncompressTarGzFiles(t, helmRepoPath) + + // point the env var to the dir containing both dapr and dapr-dashboard helm charts + t.Setenv("DAPR_HELM_REPO_URL", helmRepoPath) + + // setup tests + tests := []common.TestCase{} + tests = append(tests, common.GetTestsOnInstall(currentVersionDetails, common.TestOptions{ + HAEnabled: false, + MTLSEnabled: false, + ApplyComponentChanges: true, + CheckResourceExists: map[common.Resource]bool{ + common.CustomResourceDefs: true, + common.ClusterRoles: true, + common.ClusterRoleBindings: true, + }, + })...) + + tests = append(tests, common.GetTestsOnUninstall(currentVersionDetails, common.TestOptions{ + CheckResourceExists: map[common.Resource]bool{ + common.CustomResourceDefs: true, + common.ClusterRoles: false, + common.ClusterRoleBindings: false, + }, + })...) + + // execute tests + for _, tc := range tests { + t.Run(tc.Name, tc.Callable) + } +} + +func copyAndUncompressTarGzFiles(t *testing.T, destination string) { + // find all .tar.gz files in testdata dir + files, err := filepath.Glob(filepath.Join("testdata", "*.tgz")) + require.NoError(t, err) + + for _, file := range files { + // untar the dapr/dashboard helm .tar.gz, get back the root dir of the untarred files + // it's either 'dapr' or 'dapr-dashboard' + rootDir, err := untarDaprHelmGzFile(file, destination) + require.NoError(t, err) + + // rename the root dir to the base name of the .tar.gz file + // (eg. /var/folders/4s/w0gdrc957k11vbkgyhjrk12w0000gn/T/dapr-e2e-kube-with-env-404115459/dapr-1.12.0) + base := filepath.Base(strings.TrimSuffix(file, filepath.Ext(file))) + err = os.Rename(filepath.Join(destination, rootDir), filepath.Join(destination, base)) + require.NoError(t, err) + } +} + +func untarDaprHelmGzFile(file string, destination string) (string, error) { + // open the tar.gz file + f, err := os.Open(file) + if err != nil { + return "", err + } + defer f.Close() + + // create a gzip reader + gr, err := gzip.NewReader(f) + if err != nil { + return "", err + } + defer gr.Close() + + // create a tar reader + tr := tar.NewReader(gr) + + rootDir := "" + // iterate through all the files in the tarball + for { + hdr, err := tr.Next() + if err == io.EOF { + break // end of tarball + } + if err != nil { + return "", err + } + + // build the full destination path + filename := filepath.Join(destination, hdr.Name) + + // ensure the destination directory exists + dir := filepath.Dir(filename) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + return "", err + } + } + + // the root dir for all files is the same + rootDir = strings.FieldsFunc(hdr.Name, + func(c rune) bool { + return os.PathSeparator == c + })[0] + + // create the destination file + dstFile, err := os.Create(filename) + if err != nil { + return "", err + } + defer dstFile.Close() + + // copy the file contents + if _, err := io.Copy(dstFile, tr); err != nil { + return "", err + } + } + + return rootDir, nil +} diff --git a/tests/e2e/kubernetes/testdata/dapr-1.12.0.tgz b/tests/e2e/kubernetes/testdata/dapr-1.12.0.tgz new file mode 100644 index 000000000..4d883cc90 Binary files /dev/null and b/tests/e2e/kubernetes/testdata/dapr-1.12.0.tgz differ diff --git a/tests/e2e/kubernetes/testdata/dapr-dashboard-0.14.0.tgz b/tests/e2e/kubernetes/testdata/dapr-dashboard-0.14.0.tgz new file mode 100644 index 000000000..b380e8d56 Binary files /dev/null and b/tests/e2e/kubernetes/testdata/dapr-dashboard-0.14.0.tgz differ diff --git a/utils/utils.go b/utils/utils.go index 483bf85f7..eecc55bb8 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -351,13 +351,16 @@ func GetVersionAndImageVariant(imageTag string) (string, string) { return imageTag, "" } -// Returns true if the given file path is valid. -func ValidateFilePath(filePath string) error { - if filePath != "" { - if _, err := os.Stat(filePath); err != nil { - return fmt.Errorf("error in getting the file info for %s: %w", filePath, err) - } +// Returns no error if the given file/directory path is valid. +func ValidatePath(path string) error { + if path == "" { + return nil } + + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("error in getting the file info for %s: %w", path, err) + } + return nil } @@ -409,7 +412,7 @@ func ReadFile(filePath string) ([]byte, error) { // FindFileInDir finds and returns the path of the given file name in the given directory. func FindFileInDir(dirPath, fileName string) (string, error) { filePath := filepath.Join(dirPath, fileName) - if err := ValidateFilePath(filePath); err != nil { + if err := ValidatePath(filePath); err != nil { return "", fmt.Errorf("error in validating the file path %q: %w", filePath, err) } return filePath, nil @@ -430,3 +433,19 @@ func AttachJobObjectToProcess(pid string, proc *os.Process) { func GetJobObjectNameFromPID(pid string) string { return pid + "-" + windowsDaprAppProcJobName } + +func DiscoverHelmPath(helmPath, release, version string) (string, error) { + // first try for a local directory path. + dirPath := filepath.Join(helmPath, fmt.Sprintf("%s-%s", release, version)) + if ValidatePath(dirPath) == nil { + return dirPath, nil + } + + // not a dir, try a .tgz file instead. + filePath := filepath.Join(helmPath, fmt.Sprintf("%s-%s.tgz", release, version)) + if ValidatePath(filePath) == nil { + return filePath, nil + } + + return "", fmt.Errorf("unable to find a helm path in either %s or %s", dirPath, filePath) +} diff --git a/utils/utils_test.go b/utils/utils_test.go index d89ea4d0a..fcb962951 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -175,10 +175,12 @@ func TestGetVersionAndImageVariant(t *testing.T) { } } -func TestValidateFilePaths(t *testing.T) { +func TestValidatePaths(t *testing.T) { dirName := createTempDir(t, "test_validate_paths") - defer cleanupTempDir(t, dirName) validFile := createTempFile(t, dirName, "valid_test_file.yaml") + t.Cleanup(func() { + cleanupTempDir(t, dirName) + }) testcases := []struct { name string input string @@ -194,6 +196,11 @@ func TestValidateFilePaths(t *testing.T) { input: validFile, expectedErr: false, }, + { + name: "valid directory path", + input: dirName, + expectedErr: false, + }, { name: "invalid file path", input: "invalid_file_path", @@ -205,7 +212,7 @@ func TestValidateFilePaths(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - actual := ValidateFilePath(tc.input) + actual := ValidatePath(tc.input) assert.Equal(t, tc.expectedErr, actual != nil) }) }