diff --git a/encrypt/encrypt.go b/encrypt/encrypt.go index 2ca268fa9d..8e1dc4e8b5 100644 --- a/encrypt/encrypt.go +++ b/encrypt/encrypt.go @@ -21,6 +21,8 @@ type Parameters struct { AgentPodSelector string NodeName string PerNodeDetails bool + IPsecKeyAuthAlgo string + IPsecKeyPerNode string Writer io.Writer WaitDuration time.Duration Output string diff --git a/encrypt/ipsec_key_rotator.go b/encrypt/ipsec_key_rotator.go new file mode 100644 index 0000000000..35b20f03a6 --- /dev/null +++ b/encrypt/ipsec_key_rotator.go @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package encrypt + +var rotators = map[string]func(key ipsecKey) (ipsecKey, error){ + "": func(key ipsecKey) (ipsecKey, error) { return key.rotate() }, + "gcm-aes": newGcmAesKey, + "hmac-md5": newHmacMD5Key, + "hmac-sha1": newHmacSHA1Key, + "hmac-sha256": newHmacSHA256Key, + "hmac-sha512": newHmacSHA512Key, +} + +func IsIPsecAlgoSupported(algo string) bool { + _, ok := rotators[algo] + return ok +} + +func rotateIPsecKey(key ipsecKey, algo string) (ipsecKey, error) { + return rotators[algo](key) +} + +func newGcmAesKey(key ipsecKey) (ipsecKey, error) { + authKey, err := generateRandomHex(40) + if err != nil { + return ipsecKey{}, err + } + newKey := ipsecKey{ + spi: key.nextSPI(), + spiSuffix: key.spiSuffix, + algo: "rfc4106(gcm(aes))", + key: authKey, + size: 128, + } + return newKey, nil +} + +func newHmacMD5Key(key ipsecKey) (ipsecKey, error) { + return newCbcAesKey(key, "hmac(md5)", 16, 32) +} + +func newHmacSHA1Key(key ipsecKey) (ipsecKey, error) { + return newCbcAesKey(key, "hmac(sha1)", 20, 32) +} + +func newHmacSHA256Key(key ipsecKey) (ipsecKey, error) { + return newCbcAesKey(key, "hmac(sha256)", 32, 32) +} + +func newHmacSHA512Key(key ipsecKey) (ipsecKey, error) { + return newCbcAesKey(key, "hmac(sha512)", 64, 32) +} + +func newCbcAesKey(key ipsecKey, algo string, authKeylen int, cipherKeyLen int) (ipsecKey, error) { + authKey, err := generateRandomHex(authKeylen) + if err != nil { + return ipsecKey{}, err + } + cipherKey, err := generateRandomHex(cipherKeyLen) + if err != nil { + return ipsecKey{}, err + } + newKey := ipsecKey{ + spi: key.nextSPI(), + spiSuffix: key.spiSuffix, + algo: algo, + key: authKey, + cipherMode: "cbc(aes)", + cipherKey: cipherKey, + } + return newKey, nil +} diff --git a/encrypt/ipsec_key_rotator_test.go b/encrypt/ipsec_key_rotator_test.go new file mode 100644 index 0000000000..10229938ee --- /dev/null +++ b/encrypt/ipsec_key_rotator_test.go @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package encrypt + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_IsIPsecAlgoSupported(t *testing.T) { + testCases := []struct { + have string + expected bool + }{ + { + have: "", + expected: true, + }, + { + have: "gcm-aes", + expected: true, + }, + { + have: "hmac-md5", + expected: true, + }, + { + have: "hmac-sha1", + expected: true, + }, + { + have: "hmac-sha256", + expected: true, + }, + { + have: "hmac-sha512", + expected: true, + }, + { + have: "bla-bla", + expected: false, + }, + } + + for _, tt := range testCases { + // function to test + actual := IsIPsecAlgoSupported(tt.have) + + require.Equal(t, tt.expected, actual) + } +} + +func Test_rotateIPsecKey(t *testing.T) { + testCases := []struct { + haveKey ipsecKey + haveAlgo string + expected ipsecKey + }{ + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 3, + spiSuffix: false, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + expected: ipsecKey{ + spi: 4, + spiSuffix: false, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 16, + spiSuffix: false, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + expected: ipsecKey{ + spi: 1, + spiSuffix: false, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 3, + spiSuffix: true, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + expected: ipsecKey{ + spi: 4, + spiSuffix: true, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 16, + spiSuffix: true, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + expected: ipsecKey{ + spi: 1, + spiSuffix: true, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 3, + spiSuffix: false, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + expected: ipsecKey{ + spi: 4, + spiSuffix: false, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 16, + spiSuffix: false, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + expected: ipsecKey{ + spi: 1, + spiSuffix: false, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 3, + spiSuffix: true, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + expected: ipsecKey{ + spi: 4, + spiSuffix: true, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + }, + { + haveAlgo: "", + haveKey: ipsecKey{ + spi: 16, + spiSuffix: true, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + expected: ipsecKey{ + spi: 1, + spiSuffix: true, + algo: "hmac(sha256)", + key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", + cipherMode: "cbc(aes)", + cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", + }, + }, + { + haveAlgo: "gcm-aes", + haveKey: ipsecKey{ + spi: 16, + spiSuffix: true, + }, + expected: ipsecKey{ + spi: 1, + spiSuffix: true, + algo: "rfc4106(gcm(aes))", + key: "41049390e1e2b5d6543901daab6435f4042155fe", + size: 128, + }, + }, + { + haveAlgo: "hmac-md5", + haveKey: ipsecKey{ + spi: 1, + spiSuffix: true, + }, + expected: ipsecKey{ + spi: 2, + spiSuffix: true, + algo: "hmac(md5)", + key: "1286b7f6f9f61a4f", + cipherMode: "cbc(aes)", + cipherKey: "efbeeb4230992f76a6e4cc2ff995b756", + }, + }, + { + haveAlgo: "hmac-sha1", + haveKey: ipsecKey{ + spi: 2, + spiSuffix: true, + }, + expected: ipsecKey{ + spi: 3, + spiSuffix: true, + algo: "hmac(sha1)", + key: "5448dd20e4528a9c2d5b", + cipherMode: "cbc(aes)", + cipherKey: "123d17f2bbbae8009d952b4d0d656f06", + }, + }, + { + haveAlgo: "hmac-sha256", + haveKey: ipsecKey{ + spi: 3, + spiSuffix: true, + }, + expected: ipsecKey{ + spi: 4, + spiSuffix: true, + algo: "hmac(sha256)", + key: "a9d204b6c2df6f0b707bbfdb71b4bd44", + cipherMode: "cbc(aes)", + cipherKey: "9bd24c14452783bb6f3c9335aff2ed2e", + }, + }, + { + haveAlgo: "hmac-sha512", + haveKey: ipsecKey{ + spi: 4, + spiSuffix: true, + }, + expected: ipsecKey{ + spi: 5, + spiSuffix: true, + algo: "hmac(sha512)", + key: "8b4d92bf9396e7febb4d51e87394bb158ebcc0d9d57e4da8e938b0e931223ec7", + cipherMode: "cbc(aes)", + cipherKey: "0151a41da39e3310d4f58b3930788dc4", + }, + }, + } + + for _, tt := range testCases { + // function to test + actual, err := rotateIPsecKey(tt.haveKey, tt.haveAlgo) + + require.NoError(t, err) + require.Equal(t, tt.expected.spi, actual.spi) + require.Equal(t, tt.expected.spiSuffix, actual.spiSuffix) + require.Equal(t, tt.expected.algo, actual.algo) + require.Equal(t, len(tt.expected.key), len(actual.key)) + require.Equal(t, len(tt.expected.cipherKey), len(actual.cipherKey)) + require.Equal(t, tt.expected.size, actual.size) + require.Equal(t, tt.expected.cipherMode, actual.cipherMode) + if tt.expected.cipherMode == "" { + // this field will be randomly generated, `require.NotEqual` used for verification + require.NotEqual(t, tt.expected.key, actual.key) + require.Equal(t, tt.expected.cipherKey, actual.cipherKey) + } else { + // the following fields will be randomly generated, `require.NotEqual` used for verification + require.NotEqual(t, tt.expected.key, actual.key) + require.NotEqual(t, tt.expected.cipherKey, actual.cipherKey) + } + } +} diff --git a/encrypt/ipsec_rotate_key.go b/encrypt/ipsec_rotate_key.go index cfcb8da7f8..e0a32afcc0 100644 --- a/encrypt/ipsec_rotate_key.go +++ b/encrypt/ipsec_rotate_key.go @@ -15,6 +15,7 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/cilium/cilium-cli/defaults" + "github.com/cilium/cilium-cli/internal/utils" ) type ipsecKey struct { @@ -37,16 +38,24 @@ func (s *Encrypt) IPsecRotateKey(ctx context.Context) error { return fmt.Errorf("failed to fetch IPsec secret: %s", err) } - key, err := ipsecKeyFromString(string(secret.Data["keys"])) + keyBytes, ok := secret.Data["keys"] + if !ok { + return fmt.Errorf("IPsec key not found in the secret: %s", defaults.EncryptionSecretName) + } + key, err := ipsecKeyFromString(string(keyBytes)) if err != nil { return err } - newKey, err := key.rotate() + newKey, err := rotateIPsecKey(key, s.params.IPsecKeyAuthAlgo) if err != nil { return fmt.Errorf("failed to rotate IPsec key: %s", err) } + if s.params.IPsecKeyPerNode != "" { + newKey.spiSuffix = utils.MustParseBool(s.params.IPsecKeyPerNode) + } + patch := []byte(`{"stringData":{"keys":"` + newKey.String() + `"}}`) _, err = s.client.PatchSecret(ctx, s.params.CiliumNamespace, defaults.EncryptionSecretName, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) if err != nil { @@ -153,12 +162,8 @@ func (k ipsecKey) rotate() (ipsecKey, error) { } } - spi := k.spi + 1 - if spi >= maxIPsecSPI { - spi = 1 - } newKey := ipsecKey{ - spi: spi, + spi: k.nextSPI(), spiSuffix: k.spiSuffix, algo: k.algo, key: key, @@ -169,6 +174,14 @@ func (k ipsecKey) rotate() (ipsecKey, error) { return newKey, nil } +func (k ipsecKey) nextSPI() int { + spi := k.spi + 1 + if spi >= maxIPsecSPI { + spi = 1 + } + return spi +} + func generateRandomHex(size int) (string, error) { buf := make([]byte, size/2) if _, err := rand.Read(buf); err != nil { diff --git a/encrypt/ipsec_rotate_key_test.go b/encrypt/ipsec_rotate_key_test.go index 5d7cc7d4fb..3a6fe1147d 100644 --- a/encrypt/ipsec_rotate_key_test.go +++ b/encrypt/ipsec_rotate_key_test.go @@ -128,204 +128,3 @@ func Test_ipsecKey_String(t *testing.T) { } } - -func Test_ipsecKey_rotate(t *testing.T) { - testCases := []struct { - have ipsecKey - expected ipsecKey - }{ - { - have: ipsecKey{ - spi: 3, - spiSuffix: false, - algo: "rfc4106(gcm(aes))", - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - expected: ipsecKey{ - spi: 4, - spiSuffix: false, - algo: "rfc4106(gcm(aes))", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - }, - { - have: ipsecKey{ - spi: 16, - spiSuffix: false, - algo: "rfc4106(gcm(aes))", - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - expected: ipsecKey{ - spi: 1, - spiSuffix: false, - algo: "rfc4106(gcm(aes))", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - }, - { - have: ipsecKey{ - spi: 3, - spiSuffix: true, - algo: "rfc4106(gcm(aes))", - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - expected: ipsecKey{ - spi: 4, - spiSuffix: true, - algo: "rfc4106(gcm(aes))", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - }, - { - have: ipsecKey{ - spi: 16, - spiSuffix: true, - algo: "rfc4106(gcm(aes))", - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - expected: ipsecKey{ - spi: 1, - spiSuffix: true, - algo: "rfc4106(gcm(aes))", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "41049390e1e2b5d6543901daab6435f4042155fe", - size: 128, - cipherMode: "", - cipherKey: "", - }, - }, - { - have: ipsecKey{ - spi: 3, - spiSuffix: false, - algo: "hmac(sha256)", - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - expected: ipsecKey{ - spi: 4, - spiSuffix: false, - algo: "hmac(sha256)", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - // this field will be randomly generated, `require.NotEqual` used for verification - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - }, - { - have: ipsecKey{ - spi: 16, - spiSuffix: false, - algo: "hmac(sha256)", - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - expected: ipsecKey{ - spi: 1, - spiSuffix: false, - algo: "hmac(sha256)", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - // this field will be randomly generated, `require.NotEqual` used for verification - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - }, - { - have: ipsecKey{ - spi: 3, - spiSuffix: true, - algo: "hmac(sha256)", - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - expected: ipsecKey{ - spi: 4, - spiSuffix: true, - algo: "hmac(sha256)", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - // this field will be randomly generated, `require.NotEqual` used for verification - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - }, - { - have: ipsecKey{ - spi: 16, - spiSuffix: true, - algo: "hmac(sha256)", - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - expected: ipsecKey{ - spi: 1, - spiSuffix: true, - algo: "hmac(sha256)", - // this field will be randomly generated, `require.NotEqual` used for verification - key: "e6b4bab427cd37bb64b39cd66a8476a62963174b78bc544fb525f4c2f548342b", - size: 0, - cipherMode: "cbc(aes)", - // this field will be randomly generated, `require.NotEqual` used for verification - cipherKey: "0f12337d9ee75095ff21402dc98476f5f9107261073b70bb37747237d2691d3e", - }, - }, - } - - for _, tt := range testCases { - // function to test - actual, err := tt.have.rotate() - - require.NoError(t, err) - require.Equal(t, tt.expected.spi, actual.spi) - require.Equal(t, tt.expected.spiSuffix, actual.spiSuffix) - require.Equal(t, tt.expected.algo, actual.algo) - require.Equal(t, len(tt.expected.key), len(actual.key)) - require.Equal(t, len(tt.expected.cipherKey), len(actual.cipherKey)) - require.Equal(t, tt.expected.size, actual.size) - require.Equal(t, tt.expected.cipherMode, actual.cipherMode) - if tt.expected.cipherMode == "" { - require.NotEqual(t, tt.expected.key, actual.key) - require.Equal(t, tt.expected.cipherKey, actual.cipherKey) - } else { - require.NotEqual(t, tt.expected.key, actual.key) - require.NotEqual(t, tt.expected.cipherKey, actual.cipherKey) - } - } -} diff --git a/internal/cli/cmd/encrypt.go b/internal/cli/cmd/encrypt.go index abe4192a18..f5c1e7eee4 100644 --- a/internal/cli/cmd/encrypt.go +++ b/internal/cli/cmd/encrypt.go @@ -5,6 +5,7 @@ package cmd import ( "context" + "fmt" "time" "github.com/spf13/cobra" @@ -58,6 +59,9 @@ func newCmdIPsecRotateKey() *cobra.Command { Long: "This command rotates IPsec encryption key in the cluster", RunE: func(cmd *cobra.Command, args []string) error { params.CiliumNamespace = namespace + if err := checkParams(params); err != nil { + fatalf("Input params are invalid: %s", err) + } s := encrypt.NewEncrypt(k8sClient, params) if err := s.IPsecRotateKey(context.Background()); err != nil { fatalf("Unable to rotate IPsec key: %s", err) @@ -65,6 +69,8 @@ func newCmdIPsecRotateKey() *cobra.Command { return nil }, } + cmd.Flags().StringVarP(¶ms.IPsecKeyAuthAlgo, "auth-algo", "", "", "IPsec key authentication algorithm (optional parameter, if omitted the current settings will be used). One of: gcm-aes, hmac-md5, hmac-sha1, hmac-sha256, hmac-sha512") + cmd.Flags().StringVarP(¶ms.IPsecKeyPerNode, "key-per-node", "", "", "IPsec key per cluster node (optional parameter, if omitted the current settings will be used). One of: true, false") cmd.Flags().DurationVar(¶ms.WaitDuration, "wait-duration", 1*time.Minute, "Maximum time to wait for result, default 1 minute") return cmd } @@ -89,3 +95,15 @@ func newCmdIPsecKeyStatus() *cobra.Command { cmd.Flags().StringVarP(¶ms.Output, "output", "o", status.OutputSummary, "Output format. One of: json, summary") return cmd } + +func checkParams(params encrypt.Parameters) error { + switch params.IPsecKeyPerNode { + case "", "true", "false": + default: + return fmt.Errorf("key-per-node has invalid value: %s", params.IPsecKeyPerNode) + } + if !encrypt.IsIPsecAlgoSupported(params.IPsecKeyAuthAlgo) { + return fmt.Errorf("auth-algo has invalid value: %s", params.IPsecKeyAuthAlgo) + } + return nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 1ad2693ce8..34de225bec 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" "github.com/blang/semver/v4" @@ -79,3 +80,11 @@ func BuildImagePath(userImage, userVersion, defaultImage, defaultVersion string) func IsInHelmMode() bool { return os.Getenv(CLIModeVariableName) != "classic" } + +func MustParseBool(v string) bool { + b, err := strconv.ParseBool(v) + if err != nil { + panic(fmt.Errorf("failed to parse string [%s] to bool: %s", v, err)) + } + return b +}