diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 04419d730..4b806e065 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -117,6 +117,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return compInstall(args, toComplete, client) }, RunE: func(_ *cobra.Command, args []string) error { + client.RenderConnected = true rel, err := runInstall(args, client, valueOpts, out) if err != nil { return err diff --git a/cmd/helm/template.go b/cmd/helm/template.go index e3c1d421f..c4df2866b 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -74,6 +74,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.DryRun = true + client.RenderConnected = false client.ReleaseName = "RELEASE-NAME" client.Replace = true // Skip the name check client.ClientOnly = !validate diff --git a/pkg/action/install.go b/pkg/action/install.go index d75f86764..2284a01cc 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -74,6 +74,7 @@ type Install struct { ClientOnly bool CreateNamespace bool DryRun bool + RenderConnected bool DisableHooks bool Replace bool Wait bool @@ -242,10 +243,10 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. rel := i.createRelease(chrt, vals) var manifestDoc *bytes.Buffer - if i.DryRun { - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResourcesLocally(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer) - } else { + if i.RenderConnected { rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer) + } else { + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResourcesLocally(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer) } // Even for errors, attach this if available if manifestDoc != nil { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 63383d778..94a1b442b 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "encoding/json" "fmt" "io/ioutil" "log" @@ -27,16 +28,23 @@ import ( "testing" "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" "helm.sh/helm/v3/internal/test" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/engine" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/time" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) +type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) +type lookupMatch func(apiversion string, resource string, namespace string, name string) bool type nameTemplateTestCase struct { tpl string expected string @@ -52,6 +60,55 @@ func installAction(t *testing.T) *Install { return instAction } +func toMap(in interface{}) map[string]interface{} { + var out map[string]interface{} + marshalled, _ := json.Marshal(in) + json.Unmarshal(marshalled, &out) + + return out +} + +func addLookupReturn(obj map[string]interface{}, match lookupMatch) { + originalLookupFactory := engine.NewLookupFunction + engine.NewLookupFunction = func(config *rest.Config) lookupFunc { + return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { + if match(apiversion, resource, namespace, name) { + return obj, nil + } + + return originalLookupFactory(config)(apiversion, resource, namespace, name) + } + } +} + +func matchPod(pod v1.Pod) lookupMatch { + return func(apiversion string, resource string, namespace string, name string) bool { + return apiversion == "v1" && resource == pod.TypeMeta.Kind && namespace == pod.ObjectMeta.Namespace && name == pod.ObjectMeta.Name + } +} + +func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod { + ns := v1.NamespaceDefault + if namespace != "" { + ns = namespace + } + return v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + SelfLink: "/api/v1/namespaces/default/pods/" + name, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{{ + Name: "app:v4", + Image: "abc/app:v4", + Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}}, + }}, + }, + Status: status, + } +} + func TestInstallRelease(t *testing.T) { is := assert.New(t) instAction := installAction(t) @@ -241,25 +298,47 @@ func TestInstallRelease_DryRun(t *testing.T) { is.Equal(res.Info.Description, "Dry run complete") } -// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run. -func TestInstallRelease_DryRun_Lookup(t *testing.T) { - is := assert.New(t) +func TestInstallRelease_RenderConnected_Lookup(t *testing.T) { + it := assert.New(t) instAction := installAction(t) - instAction.DryRun = true - vals := map[string]interface{}{} + instAction.cfg.RESTClientGetter = cmdtesting.NewTestFactory() + neo := newPodWithStatus("Neo", v1.PodStatus{}, "The Matricks") + instAction.RenderConnected = true + mockChart := buildChart() + addLookupReturn(toMap(neo), matchPod(neo)) + mockChart.Templates = append(mockChart.Templates, &chart.File{ + Name: "templates/lookup", + Data: []byte(fmt.Sprintf(`goodbye: {{ lookup "v1" "%s" "%s" "%s" }}`, neo.TypeMeta.Kind, neo.ObjectMeta.Namespace, neo.ObjectMeta.Name)), + }) - mockChart := buildChart(withSampleTemplates()) + res, err := instAction.Run(mockChart, map[string]interface{}{}) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + it.Contains(res.Manifest, fmt.Sprint(toMap(neo))) +} + +func TestInstallRelease_NotRenderConnected_Lookup(t *testing.T) { + it := assert.New(t) + instAction := installAction(t) + instAction.cfg.RESTClientGetter = cmdtesting.NewTestFactory() + trinity := newPodWithStatus("Trinity", v1.PodStatus{}, "The Matricks") + instAction.RenderConnected = false + mockChart := buildChart() + addLookupReturn(toMap(trinity), matchPod(trinity)) mockChart.Templates = append(mockChart.Templates, &chart.File{ Name: "templates/lookup", - Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), + Data: []byte(fmt.Sprintf(`goodbye: {{ lookup "v1" "%s" "%s" "%s" }}`, trinity.TypeMeta.Kind, trinity.ObjectMeta.Namespace, trinity.ObjectMeta.Name)), }) - res, err := instAction.Run(mockChart, vals) + res, err := instAction.Run(mockChart, map[string]interface{}{}) if err != nil { t.Fatalf("Failed install: %s", err) } - is.Contains(res.Manifest, "goodbye: map[]") + it.Contains(res.Manifest, "goodbye: map[]") + it.NotContains(res.Manifest, fmt.Sprint(toMap(trinity))) } func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index d1bf1105a..6b4dc2cf9 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -38,7 +38,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // // This function is considered deprecated, and will be renamed in Helm 4. It will no // longer be a public function. -func NewLookupFunction(config *rest.Config) lookupFunc { +var NewLookupFunction = func(config *rest.Config) lookupFunc { return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { var client dynamic.ResourceInterface c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config)