diff --git a/README.md b/README.md index 67c8aa4..55dbd93 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![diagram](./helm-2to3.png) -**Helm plugin which migrates Helm v2 configuration and releases in-place to Helm v3** +**Helm plugin which migrates and cleans up Helm v2 configuration and releases in-place to Helm v3** ## Usage @@ -55,15 +55,48 @@ $ helm 2to3 convert [flags] RELEASE Flags: ``` - --dry-run simulate a convert - -h, --help help for convert - --delete-v2-releases v2 releases are deleted after migration. By default, the v2 releases are retained - -l, --label string label to select tiller resources by (default "OWNER=TILLER") + --dry-run simulate a convert + -h, --help help for convert + --delete-v2-releases v2 releases are deleted after migration. By default, the v2 releases are retained + -l, --label string label to select tiller resources by (default "OWNER=TILLER") -s, --release-storage string v2 release storage type/object. It can be 'secrets' or 'configmaps'. This is only used with the 'tiller-out-cluster' flag (default "secrets") - -t, --tiller-ns string namespace of Tiller (default "kube-system") + -t, --tiller-ns string namespace of Tiller (default "kube-system") --tiller-out-cluster when Tiller is not running in the cluster e.g. Tillerless ``` +### Clean up Helm v2 data + +Clean up Helm v2 configuration, release data and Tiller deployment: + +```console +$ helm 2to3 cleanup [flags] + +Flags: + --dry-run simulate a command + -h, --help help for cleanup + -l, --label string label to select tiller resources by (default "OWNER=TILLER") + -s, --release-storage string v2 release storage type/object. It can be 'secrets' or 'configmaps'. This is only used with the 'tiller-out-cluster' flag (default "secrets") + -t, --tiller-ns string namespace of Tiller (default "kube-system") + --tiller-out-cluster when Tiller is not running in the cluster e.g. Tillerless +``` + +It will clean: +- Configuration (Helm home directory) +- v2 release data +- Tiller deployment + +For cleanup it uses the default Helm v2 home folder. +To override this folder you need to set the environment variable `HELM_V2_HOME`: + +```console +$ export HELM_V2_HOME=$PWD/.helm2 +$ helm 2to3 cleanup +``` + +*Warning:* The `cleanup` command will remove the Helm v2 Configuration, Release Data and Tiller Deployment. +It cleans up all releases managed by Helm v2. It will not be possible to restore them if you haven't made a backup of the releases. +Helm v2 will not be usable afterwards. + ## Install Based on the version in `plugin.yaml`, release binary will be downloaded from GitHub: diff --git a/cmd/cleanup.go b/cmd/cleanup.go new file mode 100644 index 0000000..4aa0c47 --- /dev/null +++ b/cmd/cleanup.go @@ -0,0 +1,130 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm-2to3/pkg/v2" +) + +/*var ( + settings *EnvSettings +)*/ + +func newCleanupCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "cleanup", + Short: "cleanup Helm v2 configuration, release data and Tiller deployment", + Args: func(cmd *cobra.Command, args []string) error { + return nil + }, + RunE: runCleanup, + } + + flags := cmd.Flags() + settings.AddFlags(flags) + + return cmd +} + +func runCleanup(cmd *cobra.Command, args []string) error { + return Cleanup() +} + +// Cleanup will delete all release data for in specified namespace and owner label. It will remove +// the Tiller server deployed as per namespace and owner label. It is also delete the Helm gv2 home directory +// which contains the Helm configuration. Helm v2 will be unusable after this operation. +func Cleanup() error { + if settings.dryRun { + fmt.Printf("NOTE: This is in dry-run mode, the following actions will not be executed.\n") + fmt.Printf("Run without --dry-run to take the actions described below:\n\n") + } + + fmt.Printf("WARNING: Helm v2 Configuration, Release Data and Tiller Deployment will be removed.\n") + fmt.Printf("This will clean up all releases managed by Helm v2. It will not be possible to restore them if you haven't made a backup of the releases.\n") + fmt.Printf("Helm v2 will not be usable afterwards.\n\n") + + doCleanup, err := askConfirmation() + if err != nil { + return err + } + if !doCleanup { + return fmt.Errorf("Cleanup will not proceed as the user didn't answer (Y|y) in order to continue") + } + + fmt.Printf("\nHelm v2 data will be cleaned up.\n") + + fmt.Printf("[Helm 2] Releases will be deleted.\n") + retrieveOptions := v2.RetrieveOptions{ + ReleaseName: "", + TillerNamespace: settings.tillerNamespace, + TillerLabel: settings.label, + TillerOutCluster: settings.tillerOutCluster, + StorageType: settings.releaseStorage, + } + err = v2.DeleteAllReleaseVersions(retrieveOptions, settings.dryRun) + if err != nil { + return err + } + if !settings.dryRun { + fmt.Printf("[Helm 2] Releases deleted.\n") + } + + if !settings.tillerOutCluster { + fmt.Printf("[Helm 2] Tiller service in \"%s\" namespace will be removed.\n", settings.tillerNamespace) + err = v2.RemoveTiller(settings.tillerNamespace, settings.dryRun) + if err != nil { + return err + } + if !settings.dryRun { + fmt.Printf("[Helm 2] Tiller service in \"%s\" namespace removed.\n", settings.tillerNamespace) + } + } + + err = v2.RemoveHomeFolder(settings.dryRun) + if err != nil { + return err + } + + if !settings.dryRun { + fmt.Printf("Helm v2 data was cleaned up successfully.\n") + } + return nil +} + +func askConfirmation() (bool, error) { + fmt.Printf("[Cleanup/confirm] Are you sure you want to cleanup Helm v2 data? [y/N]: ") + + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + if err := scanner.Err(); err != nil { + return false, errors.Wrap(err, "couldn't read from standard input") + } + answer := scanner.Text() + if strings.ToLower(answer) == "y" || strings.ToLower(answer) == "yes" { + return true, nil + } + return false, nil +} diff --git a/cmd/convert.go b/cmd/convert.go index 81f7171..db3bb17 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -30,12 +30,7 @@ import ( ) var ( - tillerNamespace string - label string - releaseStorage string - dryRun bool deletev2Releases bool - tillerOutCluster bool ) func newConvertCmd(out io.Writer) *cobra.Command { @@ -53,12 +48,9 @@ func newConvertCmd(out io.Writer) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&tillerNamespace, "tiller-ns", "t", "kube-system", "namespace of Tiller") - flags.StringVarP(&label, "label", "l", "OWNER=TILLER", "label to select tiller resources by") - flags.BoolVar(&dryRun, "dry-run", false, "simulate a convert") + settings.AddFlags(flags) + flags.BoolVar(&deletev2Releases, "delete-v2-releases", false, "v2 releases are deleted after migration. By default, the v2 releases are retained") - flags.BoolVar(&tillerOutCluster, "tiller-out-cluster", false, "when Tiller is not running in the cluster e.g. Tillerless") - flags.StringVarP(&releaseStorage, "release-storage", "s", "secrets", "v2 release storage type/object. It can be 'secrets' or 'configmaps'. This is only used with the 'tiller-out-cluster' flag") return cmd @@ -66,7 +58,7 @@ func newConvertCmd(out io.Writer) *cobra.Command { func run(cmd *cobra.Command, args []string) error { releaseName := args[0] - if releaseStorage != "configmaps" && releaseStorage != "secrets" { + if settings.releaseStorage != "configmaps" && settings.releaseStorage != "secrets" { return errors.New("release-storage flag needs to be 'configmaps' or 'secrets'") } return Convert(releaseName) @@ -75,9 +67,9 @@ func run(cmd *cobra.Command, args []string) error { // Convert converts Helm 2 release into Helm 3 release. It maps the Helm v2 release versions // of the release into Helm v3 equivalent and stores the release versions. The underlying Kubernetes resources // are untouched. Note: The namespaces of each release version need to exist in the Kubernetes cluster. -// The Helm 2 release is retained by default, unless the '--deletev2Releases' flag is set. +// The Helm 2 release is retained by default, unless the '--delete-v2-releases' flag is set. func Convert(releaseName string) error { - if dryRun { + if settings.dryRun { fmt.Printf("NOTE: This is in dry-run mode, the following actions will not be executed.\n") fmt.Printf("Run without --dry-run to take the actions described below:\n\n") } @@ -88,10 +80,10 @@ func Convert(releaseName string) error { retrieveOptions := v2.RetrieveOptions{ ReleaseName: releaseName, - TillerNamespace: tillerNamespace, - TillerLabel: label, - TillerOutCluster: tillerOutCluster, - StorageType: releaseStorage, + TillerNamespace: settings.tillerNamespace, + TillerLabel: settings.label, + TillerOutCluster: settings.tillerOutCluster, + StorageType: settings.releaseStorage, } v2Releases, err := v2.GetReleaseVersions(retrieveOptions) if err != nil { @@ -101,36 +93,36 @@ func Convert(releaseName string) error { versions := []int32{} for i := len(v2Releases) - 1; i >= 0; i-- { v2Release := v2Releases[i] - version := v2Release.Version - fmt.Printf("[Helm 3] ReleaseVersion \"%s\" will be created.\n", getReleaseVersionName(releaseName, version)) - if !dryRun { + relVerName := v2.GetReleaseVersionName(releaseName, v2Release.Version) + fmt.Printf("[Helm 3] ReleaseVersion \"%s\" will be created.\n", relVerName) + if !settings.dryRun { if err := createV3ReleaseVersion(v2Release); err != nil { return err } - fmt.Printf("[Helm 3] ReleaseVersion \"%s\" created.\n", getReleaseVersionName(releaseName, version)) + fmt.Printf("[Helm 3] ReleaseVersion \"%s\" created.\n", relVerName) } - versions = append(versions, version) + versions = append(versions, v2Release.Version) } - if !dryRun { + if !settings.dryRun { fmt.Printf("[Helm 3] Release \"%s\" created.\n", releaseName) } if deletev2Releases { fmt.Printf("[Helm 2] Release \"%s\" will be deleted.\n", releaseName) deleteOptions := v2.DeleteOptions{ - DryRun: dryRun, + DryRun: settings.dryRun, Versions: versions, } if err := v2.DeleteReleaseVersions(retrieveOptions, deleteOptions); err != nil { return err } - if !dryRun { + if !settings.dryRun { fmt.Printf("[Helm 2] Release \"%s\" deleted.\n", releaseName) fmt.Printf("Release \"%s\" was converted successfully from Helm 2 to Helm 3.\n", releaseName) } } else { - if !dryRun { + if !settings.dryRun { fmt.Printf("Release \"%s\" was converted successfully from Helm 2 to Helm 3. Note: the v2 releases still remain and should be removed to avoid conflicts with the migrated v3 releases.\n", releaseName) } } @@ -145,7 +137,3 @@ func createV3ReleaseVersion(v2Release *v2rel.Release) error { } return v3.StoreRelease(v3Release) } - -func getReleaseVersionName(releaseName string, releaseVersion int32) string { - return fmt.Sprintf("%s.v%d", releaseName, releaseVersion) -} diff --git a/cmd/environment.go b/cmd/environment.go new file mode 100644 index 0000000..24cc94f --- /dev/null +++ b/cmd/environment.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "github.com/spf13/pflag" +) + +type EnvSettings struct { + tillerNamespace string + releaseStorage string + label string + dryRun bool + tillerOutCluster bool +} + +func New() *EnvSettings { + envSettings := EnvSettings{} + return &envSettings +} + +// AddFlags binds flags to the given flagset. +func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { + fs.StringVarP(&s.tillerNamespace, "tiller-ns", "t", "kube-system", "namespace of Tiller") + fs.BoolVar(&s.dryRun, "dry-run", false, "simulate a command") + fs.StringVarP(&s.label, "label", "l", "OWNER=TILLER", "label to select tiller resources by") + fs.BoolVar(&s.tillerOutCluster, "tiller-out-cluster", false, "when Tiller is not running in the cluster e.g. Tillerless") + fs.StringVarP(&s.releaseStorage, "release-storage", "s", "secrets", "v2 release storage type/object. It can be 'secrets' or 'configmaps'. This is only used with the 'tiller-out-cluster' flag") +} diff --git a/cmd/move_config.go b/cmd/move_config.go index 02ef965..c04f6f7 100644 --- a/cmd/move_config.go +++ b/cmd/move_config.go @@ -18,15 +18,11 @@ package cmd import ( "errors" - "fmt" "io" - "io/ioutil" - "os" - "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "helm.sh/helm/pkg/helmpath" + "helm-2to3/pkg/common" ) func newMoveConfigCmd(out io.Writer) *cobra.Command { @@ -52,168 +48,11 @@ func runMove(cmd *cobra.Command, args []string) error { return errors.New("config argument has to be specified") } - return move() + return Move() } -func v2HomeDir() string { - if homeDir, exists := os.LookupEnv("HELM_V2_HOME"); exists { - return homeDir - } - - homeDir, _ := homedir.Dir() - defaultDir := homeDir + "/.helm" - fmt.Printf("[Helm 2] Home directory: %s\n", defaultDir) - return defaultDir -} - -func v3ConfigDir() string { - if homeDir, exists := os.LookupEnv("HELM_V3_CONFIG"); exists { - return homeDir - } - - defaultDir := helmpath.ConfigPath() - fmt.Printf("[Helm 3] Config directory: %s\n", defaultDir) - return defaultDir -} - -func v3DataDir() string { - if homeDir, exists := os.LookupEnv("HELM_V3_DATA"); exists { - return homeDir - } - - defaultDir := helmpath.DataPath() - fmt.Printf("[Helm 3] Data directory: %s\n", defaultDir) - return defaultDir -} - -func move() error { - v2HomeDir := v2HomeDir() - v3ConfigDir := v3ConfigDir() - v3DataDir := v3DataDir() - - // Create Helm v3 config directory if needed - fmt.Printf("[Helm 3] Create config folder \"%s\" .\n", v3ConfigDir) - err := ensureDir(v3ConfigDir) - if err != nil { - return fmt.Errorf("[Helm 3] Failed to create config folder \"%s\" due to the following error: %s", v3ConfigDir, err) - } - fmt.Printf("[Helm 3] Config folder \"%s\" created.\n", v3ConfigDir) - - // Move repo config - v2RepoConfig := v2HomeDir + "/repository/repositories.yaml" - v3RepoConfig := v3ConfigDir + "/repositories.yaml" - fmt.Printf("[Helm 2] repositories file \"%s\" will copy to [Helm 3] config folder \"%s\" .\n", v2RepoConfig, v3RepoConfig) - err = copyFile(v2RepoConfig, v3RepoConfig) - if err != nil { - return fmt.Errorf("Failed to copy [Helm 2] repository file \"%s\" due to the following error: %s", v2RepoConfig, err) - } - fmt.Printf("[Helm 2] repositories file \"%s\" copied successfully to [Helm 3] config folder \"%s\" .\n", v2RepoConfig, v3RepoConfig) - - // Bot moving local repo as it is no longer5 supported in v3: v2HomeDir/repository/local - - // Not moving the cache as it is safer to recreate when needed - // v2HomeDir/cache and v2HomeDir/repository/cache - - // Create Helm v3 data directory if needed - fmt.Printf("[Helm 3] Create data folder \"%s\" .\n", v3DataDir) - err = ensureDir(v3DataDir) - if err != nil { - return fmt.Errorf("[Helm 3] Failed to create data folder \"%s\" due to the following error: %s", v3DataDir, err) - } - fmt.Printf("[Helm 3] data folder \"%s\" created.\n", v3DataDir) - - // Move plugins - v2Plugins := v2HomeDir + "/plugins" - v3Plugins := v3DataDir + "/plugins" - fmt.Printf("[Helm 2] plugins \"%s\" will copy to [Helm 3] data folder \"%s\" .\n", v2Plugins, v3Plugins) - err = copyDir(v2Plugins, v3Plugins) - if err != nil { - return fmt.Errorf("Failed to copy [Helm 2] plugins directory \"%s\" due to the following error: %s", v2Plugins, err) - } - fmt.Printf("[Helm 2] plugins \"%s\" copied successfully to [Helm 3] data folder \"%s\" .\n", v2Plugins, v3Plugins) - - // Move starters - v2Starters := v2HomeDir + "/starters" - v3Starters := v3DataDir + "/starters" - fmt.Printf("[Helm 2] starters \"%s\" will copy to [Helm 3] data folder \"%s\" .\n", v2Starters, v3Starters) - err = copyDir(v2Starters, v3Starters) - if err != nil { - return fmt.Errorf("Failed to copy [Helm 2] starters \"%s\" due to the following error: %s", v2Starters, err) - } - fmt.Printf("[Helm 2] starters \"%s\" copied successfully to [Helm 3] data folder \"%s\" .\n", v2Starters, v3Starters) - - return nil -} - -func copyFile(srcFileName, destFileName string) error { - input, err := ioutil.ReadFile(srcFileName) - if err != nil { - return err - } - err = ioutil.WriteFile(destFileName, input, 0644) - if err != nil { - return err - } - - return nil -} - -func copyDir(srcDirName, destDirName string) error { - err := ensureDir(destDirName) - if err != nil { - return fmt.Errorf("Failed to create folder \"%s\" due to the following error: %s", destDirName, err) - } - - directory, _ := os.Open(srcDirName) - objects, err := directory.Readdir(-1) - for _, obj := range objects { - srcFileName := srcDirName + "/" + obj.Name() - destFileName := destDirName + "/" + obj.Name() - if obj.IsDir() { - // create sub-directories - recursively - err = copyDir(srcFileName, destFileName) - if err != nil { - return fmt.Errorf("Failed to copy folder \"%s\" to folder \"%s\" due to the following error: %s", srcFileName, destFileName, err) - } - } else { - fileInfo, err := os.Lstat(srcFileName) - if err != nil { - return fmt.Errorf("Failed to check file \"%s\" stats due to the following error: %s", srcFileName, err) - } - if fileInfo.Mode()&os.ModeSymlink != 0 { - err = copySymLink(obj, srcDirName, destDirName) - if err != nil { - return fmt.Errorf("Failed to create symlink for \"%s\" due to the following error: %s", obj.Name(), err) - } - } else { - err = copyFile(srcFileName, destFileName) - if err != nil { - return fmt.Errorf("Failed to copy file \"%s\" to \"%s\" due to the following error: %s", srcFileName, destFileName, err) - } - } - } - } - - return nil -} - -func ensureDir(dirName string) error { - err := os.MkdirAll(dirName, os.ModePerm) - if err != nil || !os.IsExist(err) { - return err - } - return nil -} - -func copySymLink(fileInfo os.FileInfo, srcDirName, destDirName string) error { - originFileName, err := os.Readlink(srcDirName + "/" + fileInfo.Name()) - if err != nil { - return err - } - newSymLinkName := destDirName + "/" + fileInfo.Name() - err = os.Symlink(originFileName, newSymLinkName) - if err != nil { - return err - } - return nil +// Move copies v2 configuration to v2 configuration. It copies repository config, +// plugins and starters. It does not copy cache. +func Move() error { + return common.Copyv2HomeTov3() } diff --git a/cmd/root.go b/cmd/root.go index 5cf4b1b..6af3280 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,11 +23,15 @@ import ( "github.com/spf13/cobra" ) +var ( + settings *EnvSettings +) + func NewRootCmd(out io.Writer, args []string) *cobra.Command { cmd := &cobra.Command{ Use: "2to3", - Short: "Migrate Helm v2 configuration and releases in-place to Helm v3", - Long: "Migrate Helm v2 configuration and releases in-place to Helm v3", + Short: "Migrate and Cleanup Helm v2 configuration and releases in-place to Helm v3", + Long: "Migrate and Cleanup Helm v2 configuration and releases in-place to Helm v3", SilenceUsage: true, Args: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -39,8 +43,10 @@ func NewRootCmd(out io.Writer, args []string) *cobra.Command { flags := cmd.PersistentFlags() flags.Parse(args) + settings = new(EnvSettings) cmd.AddCommand( + newCleanupCmd(out), newConvertCmd(out), newMoveConfigCmd(out), ) diff --git a/go.mod b/go.mod index 5759acd..737e8de 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,9 @@ require ( github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/pkg/errors v0.8.1 github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.1.0 // indirect diff --git a/pkg/common/utils.go b/pkg/common/utils.go new file mode 100644 index 0000000..f0f4497 --- /dev/null +++ b/pkg/common/utils.go @@ -0,0 +1,163 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "fmt" + "io/ioutil" + "os" + + "helm-2to3/pkg/v2" + "helm-2to3/pkg/v3" +) + +// Copyv2HomeTov3 copies the v2 home directory to the v3 home directory . +// Note that this is not a direct 1-1 copy +func Copyv2HomeTov3() error { + v2HomeDir := v2.HomeDir() + fmt.Printf("[Helm 2] Home directory: %s\n", v2HomeDir) + v3ConfigDir := v3.ConfigDir() + fmt.Printf("[Helm 3] Config directory: %s\n", v3ConfigDir) + v3DataDir := v3.DataDir() + fmt.Printf("[Helm 3] Data directory: %s\n", v3DataDir) + + // Create Helm v3 config directory if needed + fmt.Printf("[Helm 3] Create config folder \"%s\" .\n", v3ConfigDir) + err := ensureDir(v3ConfigDir) + if err != nil { + return fmt.Errorf("[Helm 3] Failed to create config folder \"%s\" due to the following error: %s", v3ConfigDir, err) + } + fmt.Printf("[Helm 3] Config folder \"%s\" created.\n", v3ConfigDir) + + // Move repo config + v2RepoConfig := v2HomeDir + "/repository/repositories.yaml" + v3RepoConfig := v3ConfigDir + "/repositories.yaml" + fmt.Printf("[Helm 2] repositories file \"%s\" will copy to [Helm 3] config folder \"%s\" .\n", v2RepoConfig, v3RepoConfig) + err = copyFile(v2RepoConfig, v3RepoConfig) + if err != nil { + return fmt.Errorf("Failed to copy [Helm 2] repository file \"%s\" due to the following error: %s", v2RepoConfig, err) + } + fmt.Printf("[Helm 2] repositories file \"%s\" copied successfully to [Helm 3] config folder \"%s\" .\n", v2RepoConfig, v3RepoConfig) + + // Bot moving local repo as it is no longer5 supported in v3: v2HomeDir/repository/local + + // Not moving the cache as it is safer to recreate when needed + // v2HomeDir/cache and v2HomeDir/repository/cache + + // Create Helm v3 data directory if needed + fmt.Printf("[Helm 3] Create data folder \"%s\" .\n", v3DataDir) + err = ensureDir(v3DataDir) + if err != nil { + return fmt.Errorf("[Helm 3] Failed to create data folder \"%s\" due to the following error: %s", v3DataDir, err) + } + fmt.Printf("[Helm 3] data folder \"%s\" created.\n", v3DataDir) + + // Move plugins + v2Plugins := v2HomeDir + "/plugins" + v3Plugins := v3DataDir + "/plugins" + fmt.Printf("[Helm 2] plugins \"%s\" will copy to [Helm 3] data folder \"%s\" .\n", v2Plugins, v3Plugins) + err = copyDir(v2Plugins, v3Plugins) + if err != nil { + return fmt.Errorf("Failed to copy [Helm 2] plugins directory \"%s\" due to the following error: %s", v2Plugins, err) + } + fmt.Printf("[Helm 2] plugins \"%s\" copied successfully to [Helm 3] data folder \"%s\" .\n", v2Plugins, v3Plugins) + + // Move starters + v2Starters := v2HomeDir + "/starters" + v3Starters := v3DataDir + "/starters" + fmt.Printf("[Helm 2] starters \"%s\" will copy to [Helm 3] data folder \"%s\" .\n", v2Starters, v3Starters) + err = copyDir(v2Starters, v3Starters) + if err != nil { + return fmt.Errorf("Failed to copy [Helm 2] starters \"%s\" due to the following error: %s", v2Starters, err) + } + fmt.Printf("[Helm 2] starters \"%s\" copied successfully to [Helm 3] data folder \"%s\" .\n", v2Starters, v3Starters) + + return nil +} + +func copyFile(srcFileName, destFileName string) error { + input, err := ioutil.ReadFile(srcFileName) + if err != nil { + return err + } + err = ioutil.WriteFile(destFileName, input, 0644) + if err != nil { + return err + } + + return nil +} + +func copyDir(srcDirName, destDirName string) error { + err := ensureDir(destDirName) + if err != nil { + return fmt.Errorf("Failed to create folder \"%s\" due to the following error: %s", destDirName, err) + } + + directory, _ := os.Open(srcDirName) + objects, err := directory.Readdir(-1) + for _, obj := range objects { + srcFileName := srcDirName + "/" + obj.Name() + destFileName := destDirName + "/" + obj.Name() + if obj.IsDir() { + // create sub-directories - recursively + err = copyDir(srcFileName, destFileName) + if err != nil { + return fmt.Errorf("Failed to copy folder \"%s\" to folder \"%s\" due to the following error: %s", srcFileName, destFileName, err) + } + } else { + fileInfo, err := os.Lstat(srcFileName) + if err != nil { + return fmt.Errorf("Failed to check file \"%s\" stats due to the following error: %s", srcFileName, err) + } + if fileInfo.Mode()&os.ModeSymlink != 0 { + err = copySymLink(obj, srcDirName, destDirName) + if err != nil { + return fmt.Errorf("Failed to create symlink for \"%s\" due to the following error: %s", obj.Name(), err) + } + } else { + err = copyFile(srcFileName, destFileName) + if err != nil { + return fmt.Errorf("Failed to copy file \"%s\" to \"%s\" due to the following error: %s", srcFileName, destFileName, err) + } + } + } + } + + return nil +} + +func ensureDir(dirName string) error { + err := os.MkdirAll(dirName, os.ModePerm) + if err != nil || !os.IsExist(err) { + return err + } + return nil +} + +func copySymLink(fileInfo os.FileInfo, srcDirName, destDirName string) error { + originFileName, err := os.Readlink(srcDirName + "/" + fileInfo.Name()) + if err != nil { + return err + } + newSymLinkName := destDirName + "/" + fileInfo.Name() + err = os.Symlink(originFileName, newSymLinkName) + if err != nil { + return err + } + return nil +} diff --git a/pkg/v2/release.go b/pkg/v2/release.go index fcfb5f8..0147083 100644 --- a/pkg/v2/release.go +++ b/pkg/v2/release.go @@ -38,21 +38,23 @@ type DeleteOptions struct { Versions []int32 } -// GetReleaseVersions returns all release versions from Helm v2 storage for a specified release +// GetReleaseVersions returns all release versions from Helm v2 storage for a specified release.. +// It is based on Tiller namespace and labels like owner of storage. func GetReleaseVersions(retOpts RetrieveOptions) ([]*rls.Release, error) { releases, err := getReleases(retOpts) if err != nil { return nil, err } if len(releases) <= 0 { - return nil, fmt.Errorf("%s has no deployed releases", retOpts.ReleaseName) + return nil, fmt.Errorf("%s has no deployed releases\n", retOpts.ReleaseName) } return releases, nil } -// DeleteReleaseVersions deletes all release data from Helm v2 storage for a specified release +// DeleteReleaseVersions deletes all release data from Helm v2 storage for a specified release. +// It is based on Tiller namespace and labels like owner of storage. func DeleteReleaseVersions(retOpts RetrieveOptions, delOpts DeleteOptions) error { for _, ver := range delOpts.Versions { relVerName := fmt.Sprintf("%s.v%d", retOpts.ReleaseName, ver) @@ -68,6 +70,44 @@ func DeleteReleaseVersions(retOpts RetrieveOptions, delOpts DeleteOptions) error return nil } +// DeleteReleaseVersions deletes all release data from Helm v2 storage. +// It is based on Tiller namespace and labels like owner of storage. +func DeleteAllReleaseVersions(retOpts RetrieveOptions, dryRun bool) error { + if retOpts.TillerNamespace == "" { + retOpts.TillerNamespace = "kube-system" + } + if retOpts.TillerLabel == "" { + retOpts.TillerLabel = "OWNER=TILLER" + } + if retOpts.StorageType == "" { + retOpts.StorageType = "configmaps" + } + + // Get all release versions stored for that namespace and owner + releases, err := getReleases(retOpts) + if err != nil { + return err + } + if len(releases) <= 0 { + fmt.Printf("[Helm 2] no deployed releases for namespace: %s, owner: %s\n", retOpts.TillerNamespace, retOpts.TillerLabel) + return nil + } + + // Delete each release version from storage + for i := len(releases) - 1; i >= 0; i-- { + release := releases[i] + relVerName := GetReleaseVersionName(release.Name, release.Version) + fmt.Printf("[Helm 2] ReleaseVersion \"%s\" will be deleted.\n", relVerName) + if !dryRun { + if err := deleteRelease(retOpts, relVerName); err != nil { + return fmt.Errorf("[Helm 2] ReleaseVersion \"%s\" failed to delete with error: %s.\n", relVerName, err) + } + fmt.Printf("[Helm 2] ReleaseVersion \"%s\" deleted.\n", relVerName) + } + } + return nil +} + func getReleases(retOpts RetrieveOptions) ([]*rls.Release, error) { if retOpts.TillerNamespace == "" { retOpts.TillerNamespace = "kube-system" @@ -81,13 +121,8 @@ func getReleases(retOpts RetrieveOptions) ([]*rls.Release, error) { if retOpts.StorageType == "" { retOpts.StorageType = "configmaps" } + storage := getStorageType(retOpts) clientSet := utils.GetClientSet() - var storage string - if !retOpts.TillerOutCluster { - storage = utils.GetTillerStorage(retOpts.TillerNamespace) - } else { - storage = retOpts.StorageType - } var releases []*rls.Release switch storage { case "secrets": @@ -127,6 +162,16 @@ func getReleases(retOpts RetrieveOptions) ([]*rls.Release, error) { return releases, nil } +func getStorageType(retOpts RetrieveOptions) string { + var storage string + if !retOpts.TillerOutCluster { + storage = utils.GetTillerStorage(retOpts.TillerNamespace) + } else { + storage = retOpts.StorageType + } + return storage +} + func getRelease(itemReleaseData string) *rls.Release { data, _ := utils.DecodeRelease(itemReleaseData) return data @@ -139,13 +184,8 @@ func deleteRelease(retOpts RetrieveOptions, releaseVersionName string) error { if retOpts.StorageType == "" { retOpts.StorageType = "configmaps" } + storage := getStorageType(retOpts) clientSet := utils.GetClientSet() - var storage string - if !retOpts.TillerOutCluster { - storage = utils.GetTillerStorage(retOpts.TillerNamespace) - } else { - storage = retOpts.StorageType - } switch storage { case "secrets": return clientSet.CoreV1().Secrets(retOpts.TillerNamespace).Delete(releaseVersionName, &metav1.DeleteOptions{}) diff --git a/pkg/v2/utils.go b/pkg/v2/utils.go new file mode 100644 index 0000000..f3556d5 --- /dev/null +++ b/pkg/v2/utils.go @@ -0,0 +1,71 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "fmt" + "os" + "strings" + + utils "github.com/maorfr/helm-plugin-utils/pkg" + "github.com/mitchellh/go-homedir" +) + +// RemoveHomeFolder removes the v2 Helm home folder +func RemoveHomeFolder(dryRun bool) error { + homeDir := HomeDir() + fmt.Printf("[Helm 2] Home folder \"%s\" will be deleted.\n", homeDir) + if !dryRun { + if err := os.RemoveAll(homeDir); err != nil { + return fmt.Errorf("[Helm 2] Failed to delete \"%s\" due to the following error: %s.\n", homeDir, err) + } + fmt.Printf("[Helm 2] Home folder \"%s\" deleted.\n", homeDir) + } + return nil + +} + +// RemoveTiller removes Tiller service in a particular namespace from the cluster +func RemoveTiller(tillerNamespace string, dryRun bool) error { + if tillerNamespace == "" { + tillerNamespace = "kube-system" + } + if !dryRun { + applyCmd := []string{"kubectl", "delete", "--namespace", tillerNamespace, "deploy/tiller-deploy"} + output := utils.Execute(applyCmd) + if !strings.Contains(string(output), "\"tiller-deploy\" deleted") { + return fmt.Errorf("[Helm 2] Failed to remove Tiller service in \"%s\" namespace due to the following error: %s", tillerNamespace, string(output)) + } + } + return nil +} + +// HomeDir return the Helm home folder +func HomeDir() string { + if homeDir, exists := os.LookupEnv("HELM_V2_HOME"); exists { + return homeDir + } + + homeDir, _ := homedir.Dir() + defaultDir := homeDir + "/.helm" + return defaultDir +} + +// GetReleaseVersionName returns release version name +func GetReleaseVersionName(releaseName string, releaseVersion int32) string { + return fmt.Sprintf("%s.v%d", releaseName, releaseVersion) +} diff --git a/pkg/v3/utils.go b/pkg/v3/utils.go new file mode 100644 index 0000000..4dc2a70 --- /dev/null +++ b/pkg/v3/utils.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "os" + + "helm.sh/helm/pkg/helmpath" +) + +// ConfigDir returns the v2 config directory +func ConfigDir() string { + if homeDir, exists := os.LookupEnv("HELM_V3_CONFIG"); exists { + return homeDir + } + + defaultDir := helmpath.ConfigPath() + return defaultDir +} + +// DataDir returns the v3 data directory +func DataDir() string { + if homeDir, exists := os.LookupEnv("HELM_V3_DATA"); exists { + return homeDir + } + + defaultDir := helmpath.DataPath() + return defaultDir +} diff --git a/plugin.yaml b/plugin.yaml index 3172e35..888f135 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: "2to3" -version: "0.1.1" +version: "0.1.2" usage: "migrate Helm v2 configuration and releases in-place to Helm v3" description: "migrate Helm v2 configuration and releases in-place to Helm v3" command: "$HELM_PLUGIN_DIR/bin/2to3"