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:
parent
008e92de37
commit
6dcde20d7a
|
|
@ -132,3 +132,25 @@ jobs:
|
|||
TERM: xterm
|
||||
EXTRA_HELMFILE_FLAGS: ${{ matrix.extra-helmfile-flags }}
|
||||
run: make integration
|
||||
e2e_tests:
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install package
|
||||
run: |
|
||||
sudo apt-get -y install expect
|
||||
- name: Download built binaries
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: built-binaries-${{ github.run_id }}
|
||||
- name: Extract tar to get built binaries
|
||||
run: tar -xvf built-binaries.tar
|
||||
- name: Display built binaries
|
||||
run: ls -l helmfile diff-yamls yamldiff
|
||||
- name: Run helmfile init
|
||||
env:
|
||||
TERM: xterm
|
||||
run: bash test/e2e/helmfile-init/init_linux.sh
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/app"
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
)
|
||||
|
||||
// NewInitCmd helmfile checks and installs deps
|
||||
func NewInitCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
options := config.NewInitOptions()
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize the helmfile, includes version checking and installation of helm and plug-ins",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
initImpl := config.NewInitImpl(globalCfg, options)
|
||||
err := config.NewCLIConfigImpl(initImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initImpl.ValidateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := app.New(initImpl)
|
||||
return toCLIError(initImpl.GlobalImpl, a.Init(initImpl))
|
||||
},
|
||||
}
|
||||
f := cmd.Flags()
|
||||
f.BoolVar(&options.Force, "force", false, "Do not prompt, install dependencies required by helmfile")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -81,6 +81,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewInitCmd(globalImpl),
|
||||
NewApplyCmd(globalImpl),
|
||||
NewBuildCmd(globalImpl),
|
||||
NewCacheCmd(globalImpl),
|
||||
|
|
|
|||
|
|
@ -505,6 +505,7 @@ Available Commands:
|
|||
diff Diff releases defined in state file
|
||||
fetch Fetch charts from state file
|
||||
help Help about any command
|
||||
init Initialize the helmfile, includes version checking and installation of helm and plug-ins
|
||||
lint Lint charts from state file (helm lint)
|
||||
list List releases defined in state file
|
||||
repos Repos releases defined in state file
|
||||
|
|
@ -543,6 +544,11 @@ Flags:
|
|||
Use "helmfile [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
### init
|
||||
|
||||
The `helmfile init` sub-command checks the dependencies required for helmfile operation, such as `helm`, `helm diff plugin`, `helm secrets plugin`, `helm helm-git plugin`, `helm s3 plugin`. When it does not exist or the version is too low, it can be installed automatically.
|
||||
|
||||
|
||||
### sync
|
||||
|
||||
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any YAML file can be passed by specifying a `--file path/to/your/yaml/file` flag.
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
# vim: set tabstop=4 shiftwidth=4
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# IMPORTS -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
# determine working directory to use to relative paths irrespective of starting directory
|
||||
dir="${BASH_SOURCE%/*}"
|
||||
if [[ ! -d "${dir}" ]]; then dir="${PWD}"; fi
|
||||
|
||||
. "${dir}/../../integration/lib/output.sh"
|
||||
|
||||
helmfile="./helmfile"
|
||||
helm_dir="${PWD}/${dir}/.helm"
|
||||
helm=`which helm`
|
||||
export HELM_DATA_HOME="${helm_dir}/data"
|
||||
export HELM_HOME="${HELM_DATA_HOME}"
|
||||
export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
|
||||
export HELM_CONFIG_HOME="${helm_dir}/config"
|
||||
|
||||
function cleanup() {
|
||||
set +e
|
||||
info "Deleting ${helm_dir}"
|
||||
rm -rf ${helm_dir} # remove helm data so reinstalling plugins does not fail
|
||||
}
|
||||
|
||||
function removehelm() {
|
||||
[ -f $helm ] && rm -rf $helm
|
||||
}
|
||||
|
||||
set -e
|
||||
trap cleanup EXIT
|
||||
|
||||
removehelm
|
||||
|
||||
expect <<EOF
|
||||
set timeout -1
|
||||
spawn ${helmfile} init
|
||||
expect {
|
||||
"*y/n" {send "y\r";exp_continue}
|
||||
eof
|
||||
}
|
||||
EOF
|
||||
|
||||
helm plugin ls | grep diff || fail "helmfile init run fail"
|
||||
|
||||
all_tests_passed
|
||||
Loading…
Reference in New Issue