feat: CLI: Allow setting Helm values literal (#3601) (#3646)

* feat: CLI: Allow setting Helm values literal (#3601)

While you could already set values files using the `--values` flag with external data using the CLI with `argocd app create` and `argocd app set`, this was not yet possible for managing the literal `.spec.source.helm.values` value in an Application without resorting to a complicated `argocd app patch` escaped parameter or by generating the entire application YAML manifest by yourself.

Therefore, this PR adds a `--values-literal-file` flag to the `argocd app create` and `argocd app set` commands, which accepts a local file name or URL to a values file, which will be read and included as a multiline string in the application manifest. This is different from the `--helm-set-file` flag which expects the file in the chart itself.

The `argocd app unset` command is expanded with a `--values-literal` flag, so we can also unset this field again.

I hope I chose nice enough names for the flags, I wanted to make clear it expects a file name, but also distinguish it enough from the existing `--values` flag which actually points to values files.

Because the current `setHelmOpt()` functionality would not work for unsetting things to an empty value and it was difficult to do these changes independently, this PR also contains the fix for issue #3644. A separate PR has still been created for that one because I think it should end up as a separate issue in the release notes.

* feat: CLI: Allow setting Helm values literal, add tests
This commit is contained in:
Henno Schooljan 2020-05-29 18:00:28 +02:00 committed by GitHub
parent 2277af2f32
commit 36da074344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 96 additions and 6 deletions

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"reflect"
@ -492,6 +493,18 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
spec.RevisionHistoryLimit = &i
case "values":
setHelmOpt(&spec.Source, helmOpts{valueFiles: appOpts.valuesFiles})
case "values-literal-file":
var data []byte
// read uri
parsedURL, err := url.ParseRequestURI(appOpts.values)
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
data, err = ioutil.ReadFile(appOpts.values)
} else {
data, err = config.ReadRemoteFile(appOpts.values)
}
errors.CheckError(err)
setHelmOpt(&spec.Source, helmOpts{values: string(data)})
case "release-name":
setHelmOpt(&spec.Source, helmOpts{releaseName: appOpts.releaseName})
case "helm-set":
@ -613,6 +626,7 @@ func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) {
type helmOpts struct {
valueFiles []string
values string
releaseName string
helmSets []string
helmSetStrings []string
@ -626,6 +640,9 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
if len(opts.valueFiles) > 0 {
src.Helm.ValueFiles = opts.valueFiles
}
if len(opts.values) > 0 {
src.Helm.Values = opts.values
}
if opts.releaseName != "" {
src.Helm.ReleaseName = opts.releaseName
}
@ -684,6 +701,7 @@ type appOptions struct {
destNamespace string
parameters []string
valuesFiles []string
values string
releaseName string
helmSets []string
helmSetStrings []string
@ -716,6 +734,7 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
command.Flags().StringVar(&opts.values, "values-literal-file", "", "Filename or URL to import as a literal Helm values block")
command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name")
command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)")
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)")
@ -741,6 +760,7 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
parameters []string
valuesLiteral bool
valuesFiles []string
nameSuffix bool
namePrefix bool
@ -822,7 +842,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
}
if app.Spec.Source.Helm != nil {
if len(parameters) == 0 && len(valuesFiles) == 0 {
if len(parameters) == 0 && len(valuesFiles) == 0 && !valuesLiteral {
c.HelpFunc()(c, args)
os.Exit(1)
}
@ -836,17 +856,20 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
}
}
specValueFiles := app.Spec.Source.Helm.ValueFiles
if valuesLiteral {
app.Spec.Source.Helm.Values = ""
updated = true
}
for _, valuesFile := range valuesFiles {
specValueFiles := app.Spec.Source.Helm.ValueFiles
for i, vf := range specValueFiles {
if vf == valuesFile {
specValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
app.Spec.Source.Helm.ValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
updated = true
break
}
}
}
setHelmOpt(&app.Spec.Source, helmOpts{valueFiles: specValueFiles})
if !updated {
return
}
@ -859,8 +882,9 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
errors.CheckError(err)
},
}
command.Flags().StringArrayVarP(&parameters, "parameter", "p", []string{}, "unset a parameter override (e.g. -p guestbook=image)")
command.Flags().StringArrayVar(&valuesFiles, "values", []string{}, "unset one or more helm values files")
command.Flags().StringArrayVarP(&parameters, "parameter", "p", []string{}, "Unset a parameter override (e.g. -p guestbook=image)")
command.Flags().StringArrayVar(&valuesFiles, "values", []string{}, "Unset one or more Helm values files")
command.Flags().BoolVar(&valuesLiteral, "values-literal", false, "Unset literal Helm values block")
command.Flags().BoolVar(&nameSuffix, "namesuffix", false, "Kustomize namesuffix")
command.Flags().BoolVar(&namePrefix, "nameprefix", false, "Kustomize nameprefix")
command.Flags().BoolVar(&kustomizeVersion, "kustomize-version", false, "Kustomize version")

View file

@ -3,6 +3,8 @@ package e2e
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"testing"
@ -121,6 +123,70 @@ func TestHelmValues(t *testing.T) {
})
}
func TestHelmValuesLiteralFileLocal(t *testing.T) {
Given(t).
Path("helm").
When().
Create().
AppSet("--values-literal-file", "testdata/helm/baz.yaml").
Then().
And(func(app *Application) {
data, err := ioutil.ReadFile("testdata/helm/baz.yaml")
if err != nil {
panic(err)
}
assert.Equal(t, string(data), app.Spec.Source.Helm.Values)
}).
When().
AppUnSet("--values-literal").
Then().
And(func(app *Application) {
assert.Nil(t, app.Spec.Source.Helm)
})
}
func TestHelmValuesLiteralFileRemote(t *testing.T) {
sentinel := "a: b"
serve := func(c chan<- string) {
// listen on first available dynamic (unprivileged) port
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
// send back the address so that it can be used
c <- listener.Addr().String()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// return the sentinel text at root URL
fmt.Fprint(w, sentinel)
})
panic(http.Serve(listener, nil))
}
c := make(chan string, 1)
// run a local webserver to test data retrieval
go serve(c)
address := <-c
t.Logf("Listening at address: %s", address)
Given(t).
Path("helm").
When().
Create().
AppSet("--values-literal-file", "http://"+address).
Then().
And(func(app *Application) {
assert.Equal(t, "a: b", app.Spec.Source.Helm.Values)
}).
When().
AppUnSet("--values-literal").
Then().
And(func(app *Application) {
assert.Nil(t, app.Spec.Source.Helm)
})
}
func TestHelmCrdHook(t *testing.T) {
Given(t).
Path("helm-crd").