diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 967596670f..534e0d00b2 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -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(¶meters, "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(¶meters, "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") diff --git a/test/e2e/helm_test.go b/test/e2e/helm_test.go index 33494e88ae..17c7900bf9 100644 --- a/test/e2e/helm_test.go +++ b/test/e2e/helm_test.go @@ -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").