Add subcommand init for checks and installs helmfile deps (#389)

* Add subcommand init for checks and installs helmfile deps

Signed-off-by: xiaomudk <xiaomudk@gmail.com>
This commit is contained in:
xiaomudk
2022-11-03 14:51:30 +08:00
committed by GitHub
parent 008e92de37
commit 6dcde20d7a
11 changed files with 501 additions and 5 deletions
+8
View File
@@ -93,6 +93,14 @@ func Init(app *App) *App {
return app
}
func (a *App) Init(c InitConfigProvider) error {
runner := &helmexec.ShellRunner{
Logger: a.Logger,
}
helmfileInit := NewHelmfileInit(a.OverrideHelmBinary, c, a.Logger, runner)
return helmfileInit.Initialize()
}
func (a *App) Deps(c DepsConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
prepErr := run.withPreparedCharts("deps", state.ChartPrepareOptions{
+4
View File
@@ -247,6 +247,10 @@ type ListConfigProvider interface {
type CacheConfigProvider interface{}
type InitConfigProvider interface {
Force() bool
}
// when enable reuse-values, reuse the last release's values and merge in any overrides values.
type valuesControlMode interface {
ReuseValues() bool
+238
View File
@@ -0,0 +1,238 @@
package app
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"github.com/Masterminds/semver/v3"
"go.uber.org/zap"
"helm.sh/helm/v3/pkg/cli"
"github.com/helmfile/helmfile/pkg/helmexec"
)
const (
HelmRequiredVersion = "v2.10.0"
HelmRecommendedVersion = "v3.10.1"
HelmDiffRecommendedVersion = "v3.4.0"
HelmSecretsRecommendedVersion = "v4.1.1"
HelmGitRecommendedVersion = "v0.12.0"
HelmS3RecommendedVersion = "v0.14.0"
HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3"
)
var (
manuallyInstallCode = 1
windowPackageManagers = map[string]string{
"scoop": fmt.Sprintf("scoop install helm@%s", strings.TrimLeft(HelmRecommendedVersion, "v")),
"choco": fmt.Sprintf("choco install kubernetes-helm --version %s", strings.TrimLeft(HelmRecommendedVersion, "v")),
}
helmPlugins = []helmRecommendedPlugin{
{
name: "diff",
version: HelmDiffRecommendedVersion,
repo: "https://github.com/databus23/helm-diff",
},
{
name: "secrets",
version: HelmSecretsRecommendedVersion,
repo: "https://github.com/jkroepke/helm-secrets",
},
{
name: "s3",
version: HelmS3RecommendedVersion,
repo: "https://github.com/hypnoglow/helm-s3.git",
},
{
name: "helm-git",
version: HelmGitRecommendedVersion,
repo: "https://github.com/aslafy-z/helm-git.git",
},
}
)
type helmRecommendedPlugin struct {
name string
version string
repo string
}
type HelmfileInit struct {
helmBinary string
configProvider InitConfigProvider
logger *zap.SugaredLogger
runner helmexec.Runner
}
func downloadfile(filepath string, url string) error {
file, err := os.Create(filepath)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
resp, err := http.Get(url)
if err != nil {
return err
}
if resp.StatusCode/100 != 2 {
return fmt.Errorf("download %s error, code: %d", url, resp.StatusCode)
}
defer func() { _ = resp.Body.Close() }()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}
func NewHelmfileInit(helmBinary string, c InitConfigProvider, logger *zap.SugaredLogger, runner helmexec.Runner) *HelmfileInit {
return &HelmfileInit{helmBinary: helmBinary, configProvider: c, logger: logger, runner: runner}
}
func (h *HelmfileInit) UpdateHelm() error {
return h.InstallHelm()
}
func (h *HelmfileInit) installHelmOnWindows() error {
for name, command := range windowPackageManagers {
_, err := exec.LookPath(name)
if err != nil {
continue
}
err = h.WhetherContinue(fmt.Sprintf("use: '%s'", command))
if err != nil {
return err
}
_, err = h.runner.Execute("cmd", []string{
"/c",
command,
}, nil, true)
return err
}
return &Error{msg: "windows platform, please install helm manually, installation steps: https://helm.sh/docs/intro/install/", code: &manuallyInstallCode}
}
func (h *HelmfileInit) InstallHelm() error {
if runtime.GOOS == "windows" {
return h.installHelmOnWindows()
}
err := h.WhetherContinue(fmt.Sprintf("use: '%s'", HelmInstallCommand))
if err != nil {
return err
}
getHelmScript, err := os.CreateTemp("", "get-helm-3.sh")
defer func() {
_ = getHelmScript.Close()
_ = os.Remove(getHelmScript.Name())
}()
if err != nil {
return err
}
err = downloadfile(getHelmScript.Name(), HelmInstallCommand)
if err != nil {
return err
}
_, err = h.runner.Execute("bash", []string{
getHelmScript.Name(),
"--version",
HelmRecommendedVersion,
}, nil, true)
if err != nil {
return err
}
h.helmBinary = DefaultHelmBinary
return nil
}
func (h *HelmfileInit) WhetherContinue(ask string) error {
if h.configProvider.Force() {
return nil
}
askYes := AskForConfirmation(ask)
if !askYes {
return &Error{msg: "cancel automatic installation, please install manually", code: &manuallyInstallCode}
}
return nil
}
func (h *HelmfileInit) CheckHelmPlugins() error {
settings := cli.New()
helm := helmexec.New(h.helmBinary, false, h.logger, "", h.runner)
for _, p := range helmPlugins {
pluginVersion, err := helmexec.GetPluginVersion(p.name, settings.PluginsDirectory)
if err != nil {
if !strings.Contains(err.Error(), "not installed") {
return err
}
err = h.WhetherContinue(fmt.Sprintf("The helm plugin %s is not installed, do you need to install it", p.name))
if err != nil {
return err
}
err = helm.AddPlugin(p.name, p.repo, p.version)
if err != nil {
return err
}
pluginVersion, _ = helmexec.GetPluginVersion(p.name, settings.PluginsDirectory)
}
requiredVersion, _ := semver.NewVersion(p.version)
if pluginVersion.LessThan(requiredVersion) {
err = h.WhetherContinue(fmt.Sprintf("The helm plugin %s version is too low, do you need to update it", p.name))
if err != nil {
return err
}
err = helm.UpdatePlugin(p.name)
if err != nil {
return err
}
}
}
return nil
}
func (h *HelmfileInit) CheckHelm() error {
helmExits := true
_, err := exec.LookPath(h.helmBinary)
if err != nil {
helmExits = false
}
if !helmExits {
h.logger.Info("helm not found, needs to be installed")
err = h.InstallHelm()
if err != nil {
return err
}
}
helmversion, err := helmexec.GetHelmVersion(h.helmBinary, h.runner)
if err != nil {
return err
}
requiredHelmVersion, _ := semver.NewVersion(HelmRequiredVersion)
if helmversion.LessThan(requiredHelmVersion) {
h.logger.Infof("helm version is too low, the current version is %s, the required version is %s", helmversion, requiredHelmVersion)
err = h.UpdateHelm()
if err != nil {
return err
}
}
return nil
}
func (h *HelmfileInit) Initialize() error {
err := h.CheckHelm()
if err != nil {
return err
}
err = h.CheckHelmPlugins()
if err != nil {
return err
}
h.logger.Info("helmfile initialization completed!")
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package app
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"regexp"
"testing"
)
func TestDownloadfile(t *testing.T) {
var ts *httptest.Server
cases := []struct {
name string
handler func(http.ResponseWriter, *http.Request)
url string
filepath string
wantContent string
wantError string
}{
{
name: "download success",
handler: func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "helmfile")
},
wantContent: "helmfile",
},
{
name: "download 404",
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprint(w, "not found")
},
wantError: "download .*? error, code: 404",
},
{
name: "download 500",
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
fmt.Fprint(w, "server error")
},
wantError: "download .*? error, code: 500",
},
{
name: "download path error",
handler: func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "helmfile")
},
filepath: "abc/down.txt",
wantError: "open .*? no such file or directory",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
dir := t.TempDir()
downfile := path.Join(dir, "down.txt")
if c.filepath != "" {
downfile = path.Join(dir, c.filepath)
}
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.handler(w, r)
}))
defer ts.Close()
url := ts.URL
if c.url != "" {
url = c.url
}
err := downloadfile(downfile, url)
if c.wantError != "" {
if err == nil {
t.Errorf("download got no error, want error: %v", c.wantError)
} else if matched, regexErr := regexp.MatchString(c.wantError, err.Error()); regexErr != nil || !matched {
t.Errorf("download got error: %v, want error: %v", err, c.wantError)
}
return
}
content, err := os.ReadFile(downfile)
if err != nil {
t.Errorf("read download file error: %v", err)
}
if string(content) != c.wantContent {
t.Errorf("download file content got: %v, want content: %v", string(content), c.wantContent)
}
})
}
}
+30
View File
@@ -0,0 +1,30 @@
package config
// InitOptions is the options for the init command
type InitOptions struct {
Force bool
}
// NewInitOptions creates a new InitOptions
func NewInitOptions() *InitOptions {
return &InitOptions{}
}
// InitImpl is impl for InitOptions
type InitImpl struct {
*GlobalImpl
*InitOptions
}
// NewInitImpl creates a new InitImpl
func NewInitImpl(g *GlobalImpl, b *InitOptions) *InitImpl {
return &InitImpl{
GlobalImpl: g,
InitOptions: b,
}
}
// Force returns the Force.
func (b *InitImpl) Force() bool {
return b.InitOptions.Force
}
+16 -2
View File
@@ -79,7 +79,7 @@ func parseHelmVersion(versionStr string) (semver.Version, error) {
return *ver, nil
}
func getHelmVersion(helmBinary string, runner Runner) (semver.Version, error) {
func GetHelmVersion(helmBinary string, runner Runner) (semver.Version, error) {
// Autodetect from `helm version`
outBytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil, false)
if err != nil {
@@ -115,7 +115,7 @@ func redactedURL(chart string) string {
// nolint: golint
func New(helmBinary string, enableLiveOutput bool, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
// TODO: proper error handling
version, err := getHelmVersion(helmBinary, runner)
version, err := GetHelmVersion(helmBinary, runner)
if err != nil {
panic(err)
}
@@ -521,6 +521,20 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin
return err
}
func (helm *execer) AddPlugin(name, path, version string) error {
helm.logger.Infof("Install helm plugin %v", name)
out, err := helm.exec([]string{"plugin", "install", path, "--version", version}, map[string]string{}, nil)
helm.info(out)
return err
}
func (helm *execer) UpdatePlugin(name string) error {
helm.logger.Infof("Update helm plugin %v", name)
out, err := helm.exec([]string{"plugin", "update", name}, map[string]string{}, nil)
helm.info(out)
return err
}
func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {
cmdargs := args
if len(helm.extra) > 0 {