From 0a7c730eccc501f476cddb3b9606009bfb61780a Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 15 Jul 2026 11:51:58 +0200 Subject: [PATCH 1/5] implement templated agentHelmValues --- go.mod | 4 +- go.sum | 4 +- pkg/client/plural.go | 32 ++++++-- pkg/console/agent_helm_values.go | 85 ++++++++++++++++++++ pkg/console/agent_helm_values_test.go | 107 ++++++++++++++++++++++++++ pkg/console/console.go | 1 + pkg/console/settings.go | 41 ++++++++++ 7 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 pkg/console/agent_helm_values.go create mode 100644 pkg/console/agent_helm_values_test.go diff --git a/go.mod b/go.mod index 84691bd2e..154acd3e7 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c github.com/olekukonko/tablewriter v1.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/pluralsh/console/go/client v1.76.4 + github.com/pluralsh/console/go/client v1.76.5 github.com/pluralsh/console/go/polly v1.0.0 github.com/pluralsh/gqlclient v1.12.2 github.com/pluralsh/plural-operator v0.6.0 @@ -349,5 +349,3 @@ require ( sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect ) - -replace github.com/pluralsh/console/go/client => github.com/pluralsh/console/go/client v0.0.0-20260706120905-5f6ff115eb71 diff --git a/go.sum b/go.sum index d52291730..f2ed60d2d 100644 --- a/go.sum +++ b/go.sum @@ -608,8 +608,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pluralsh/console/go/client v0.0.0-20260706120905-5f6ff115eb71 h1:ZDQvTGLJEAqLmiZTffaLNp3Yc4GHRYrpxbKMppz+Z0s= -github.com/pluralsh/console/go/client v0.0.0-20260706120905-5f6ff115eb71/go.mod h1:+dJuFu2ruNb8u4Aaor8NPneOiK2ndrwGBsbsFxbQfDU= +github.com/pluralsh/console/go/client v1.76.5 h1:0DjfQTphB3pDLXVdK4Ty9/EsJvrm8kC12pqB9unvePM= +github.com/pluralsh/console/go/client v1.76.5/go.mod h1:+dJuFu2ruNb8u4Aaor8NPneOiK2ndrwGBsbsFxbQfDU= github.com/pluralsh/console/go/controller v0.0.0-20260706120905-5f6ff115eb71 h1:d00SB5O8he1ls7YmUpI10J9FiqXKU+k8EgI0gKqRdSo= github.com/pluralsh/console/go/controller v0.0.0-20260706120905-5f6ff115eb71/go.mod h1:7eM3lngWsj4OUrQsuN2ZTT5UgV7c5yi+PZF8Cer3K+M= github.com/pluralsh/console/go/polly v1.0.0 h1:NRg8CMITGllEJ08oYidNuuG/gJGDdDuga8TM+gdIcFA= diff --git a/pkg/client/plural.go b/pkg/client/plural.go index acc8dcb9c..470d03e00 100644 --- a/pkg/client/plural.go +++ b/pkg/client/plural.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/pluralsh/console/go/polly/algorithms" + consoleclient "github.com/pluralsh/console/go/client" "github.com/samber/lo" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -15,18 +16,15 @@ import ( "github.com/urfave/cli" - "github.com/pluralsh/plural-cli/pkg/common" - "github.com/pluralsh/plural-cli/pkg/scm" - "github.com/pluralsh/plural-cli/pkg/wkspace" - - "sigs.k8s.io/yaml" - "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/kubernetes" "github.com/pluralsh/plural-cli/pkg/manifest" + "github.com/pluralsh/plural-cli/pkg/scm" "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/plural-cli/pkg/wkspace" ) type Plural struct { @@ -243,7 +241,13 @@ func (p *Plural) DoInstallOperator(url, token, values, chart_loc, clusterId stri utils.Highlight("Using Agent v%s\n", version) if settings.AgentHelmValues != nil { - if err := yaml.Unmarshal([]byte(*settings.AgentHelmValues), &globalVals); err != nil { + cluster, err := p.fetchClusterForAgentHelmValues(settings, clusterId) + if err != nil { + return fmt.Errorf("fetching cluster for agent helm values templating: %w", err) + } + + globalVals, err = console.ResolveAgentHelmValues(settings, cluster) + if err != nil { return err } } else { @@ -285,6 +289,20 @@ func (p *Plural) ReinstallOperator(c *cli.Context, id, handle *string, chart_loc return p.DoInstallOperator(url, deployToken, c.String("values"), chart_loc, clusterId) } +func (p *Plural) fetchClusterForAgentHelmValues( + settings *consoleclient.DeploymentSettingsFragment, + clusterId string, +) (*consoleclient.ClusterFragment, error) { + if settings == nil || !lo.FromPtr(settings.AgentHelmValuesTemplateable) { + return nil, nil + } + if clusterId == "" { + return nil, fmt.Errorf("cluster id is required to render agent helm values") + } + + return p.ConsoleClient.GetCluster(lo.ToPtr(clusterId), nil) +} + func displayWarning(err error) { var outMsg string var ee *exec.ExitError diff --git a/pkg/console/agent_helm_values.go b/pkg/console/agent_helm_values.go new file mode 100644 index 000000000..ea4620758 --- /dev/null +++ b/pkg/console/agent_helm_values.go @@ -0,0 +1,85 @@ +package console + +import ( + "fmt" + "strings" + + gqlclient "github.com/pluralsh/console/go/client" + "github.com/pluralsh/console/go/polly/template" + "github.com/samber/lo" + "sigs.k8s.io/yaml" +) + +func ResolveAgentHelmValues( + settings *gqlclient.DeploymentSettingsFragment, + cluster *gqlclient.ClusterFragment, +) (map[string]interface{}, error) { + if settings == nil || settings.AgentHelmValues == nil { + return map[string]interface{}{}, nil + } + + raw := *settings.AgentHelmValues + rendered := raw + if lo.FromPtr(settings.AgentHelmValuesTemplateable) { + if cluster == nil { + return nil, fmt.Errorf("cluster context is required to render agent helm values") + } + bindings := agentHelmValuesBindings(cluster) + out, err := template.RenderLiquid([]byte(raw), bindings) + if err != nil { + return nil, fmt.Errorf("rendering agent helm values: %w", err) + } + rendered = string(out) + } + + globalVals := map[string]interface{}{} + if err := yaml.Unmarshal([]byte(rendered), &globalVals); err != nil { + return nil, err + } + + return globalVals, nil +} + +func agentHelmValuesBindings(cluster *gqlclient.ClusterFragment) map[string]interface{} { + return map[string]interface{}{ + "cluster": clusterBindings(cluster), + } +} + +func clusterBindings(cluster *gqlclient.ClusterFragment) map[string]interface{} { + if cluster == nil { + return map[string]interface{}{} + } + + res := map[string]interface{}{ + "ID": cluster.ID, + "Self": cluster.Self, + "Handle": cluster.Handle, + "Name": cluster.Name, + "Version": cluster.Version, + "CurrentVersion": cluster.CurrentVersion, + "KasUrl": cluster.KasURL, + "Metadata": cluster.Metadata, + "Distro": cluster.Distro, + "Tags": clusterTagsMap(cluster.Tags), + } + + for k, v := range res { + res[strings.ToLower(k)] = v + } + res["kasUrl"] = cluster.KasURL + res["currentVersion"] = cluster.CurrentVersion + + return res +} + +func clusterTagsMap(tags []*gqlclient.ClusterTags) map[string]string { + res := map[string]string{} + for _, tag := range tags { + if tag == nil { + continue + } + res[tag.Name] = tag.Value + } + return res +} diff --git a/pkg/console/agent_helm_values_test.go b/pkg/console/agent_helm_values_test.go new file mode 100644 index 000000000..b761b3742 --- /dev/null +++ b/pkg/console/agent_helm_values_test.go @@ -0,0 +1,107 @@ +package console_test + +import ( + "errors" + "testing" + + gqlclient "github.com/pluralsh/console/go/client" + "github.com/pluralsh/plural-cli/pkg/console" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsUnknownGraphQLField(t *testing.T) { + t.Parallel() + + err := errors.New(`GraphQL error: GetDeploymentSettings: Cannot query field "agentHelmValuesTemplateable" on type "DeploymentSettings".`) + assert.True(t, console.IsUnknownGraphQLField(err, "agentHelmValuesTemplateable")) + assert.False(t, console.IsUnknownGraphQLField(err, "agentHelmValues")) + assert.False(t, console.IsUnknownGraphQLField(nil, "agentHelmValuesTemplateable")) +} + +func TestResolveAgentHelmValuesWithoutTemplating(t *testing.T) { + t.Parallel() + + settings := &gqlclient.DeploymentSettingsFragment{ + AgentHelmValues: lo.ToPtr("foo: bar\n"), + } + + vals, err := console.ResolveAgentHelmValues(settings, nil) + require.NoError(t, err) + assert.Equal(t, "bar", vals["foo"]) +} + +func TestResolveAgentHelmValuesWithTemplating(t *testing.T) { + t.Parallel() + + settings := &gqlclient.DeploymentSettingsFragment{ + AgentHelmValues: lo.ToPtr("cluster:\n name: {{ cluster.name }}\n"), + AgentHelmValuesTemplateable: lo.ToPtr(true), + } + cluster := &gqlclient.ClusterFragment{ + ID: "cluster-id", + Name: "test", + } + + vals, err := console.ResolveAgentHelmValues(settings, cluster) + require.NoError(t, err) + + clusterVals, ok := vals["cluster"].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "test", clusterVals["name"]) +} + +func TestResolveAgentHelmValuesRequiresClusterWhenTemplating(t *testing.T) { + t.Parallel() + + settings := &gqlclient.DeploymentSettingsFragment{ + AgentHelmValues: lo.ToPtr("cluster:\n id: {{ cluster.id }}\n"), + AgentHelmValuesTemplateable: lo.ToPtr(true), + } + + _, err := console.ResolveAgentHelmValues(settings, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "cluster context is required") +} + +func TestResolveAgentHelmValuesRendersClusterMetadataAndTags(t *testing.T) { + t.Parallel() + + settings := &gqlclient.DeploymentSettingsFragment{ + AgentHelmValues: lo.ToPtr(`name: {{ cluster.name }} +region: {{ cluster.metadata.region }} +env: {{ cluster.tags.env }} +id: {{ cluster.id }} +kas: {{ cluster.kasUrl }} +`), + AgentHelmValuesTemplateable: lo.ToPtr(true), + } + cluster := &gqlclient.ClusterFragment{ + ID: "cluster-1", + Name: "prod-cluster", + KasURL: lo.ToPtr("https://kas.example.com"), + Metadata: map[string]any{ + "region": "eu-central-1", + }, + Tags: []*gqlclient.ClusterTags{ + {Name: "env", Value: "production"}, + }, + } + + vals, err := console.ResolveAgentHelmValues(settings, cluster) + require.NoError(t, err) + assert.Equal(t, "prod-cluster", vals["name"]) + assert.Equal(t, "eu-central-1", vals["region"]) + assert.Equal(t, "production", vals["env"]) + assert.Equal(t, "cluster-1", vals["id"]) + assert.Equal(t, "https://kas.example.com", vals["kas"]) +} + +func TestResolveAgentHelmValuesNilSettings(t *testing.T) { + t.Parallel() + + vals, err := console.ResolveAgentHelmValues(nil, nil) + require.NoError(t, err) + assert.Empty(t, vals) +} diff --git a/pkg/console/console.go b/pkg/console/console.go index 2b22a81ff..7a122219c 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -55,6 +55,7 @@ type ConsoleClient interface { CreateNotificationSinks(attr consoleclient.NotificationSinkAttributes) (*consoleclient.NotificationSinkFragment, error) UpdateDeploymentSettings(attr consoleclient.DeploymentSettingsAttributes) (*consoleclient.UpdateDeploymentSettings, error) GetGlobalSettings() (*consoleclient.DeploymentSettingsFragment, error) + GetGlobalSettingsMinimal() (*consoleclient.DeploymentSettingsFragment, error) GetAgentRun(id string) (*consoleclient.AgentRunMinimalFragment, error) ListAgentRuns(first int64) ([]*consoleclient.AgentRunMinimalFragment, error) ListStackRuns(stackID string) (*consoleclient.ListStackRuns, error) diff --git a/pkg/console/settings.go b/pkg/console/settings.go index 3a501121e..776ce436d 100644 --- a/pkg/console/settings.go +++ b/pkg/console/settings.go @@ -2,6 +2,7 @@ package console import ( "fmt" + "strings" gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/api" @@ -20,6 +21,14 @@ func (c *consoleClient) UpdateDeploymentSettings(attr gqlclient.DeploymentSettin } func (c *consoleClient) GetGlobalSettings() (*gqlclient.DeploymentSettingsFragment, error) { + settings, err := c.getGlobalSettings() + if err != nil && IsUnknownGraphQLField(err, "agentHelmValuesTemplateable") { + return c.GetGlobalSettingsMinimal() + } + return settings, err +} + +func (c *consoleClient) getGlobalSettings() (*gqlclient.DeploymentSettingsFragment, error) { resp, err := c.client.GetDeploymentSettings(c.ctx) if err != nil { return nil, api.GetErrorResponse(err, "GetDeploymentSettings") @@ -29,3 +38,35 @@ func (c *consoleClient) GetGlobalSettings() (*gqlclient.DeploymentSettingsFragme } return resp.DeploymentSettings, nil } + +func (c *consoleClient) GetGlobalSettingsMinimal() (*gqlclient.DeploymentSettingsFragment, error) { + resp, err := c.client.GetDeploymentSettingsMinimal(c.ctx) + if err != nil { + return nil, api.GetErrorResponse(err, "GetDeploymentSettingsMinimal") + } + if resp == nil { + return nil, fmt.Errorf("returned GetDeploymentSettingsMinimal object is nil") + } + return toDeploymentSettingsFragment(resp.DeploymentSettings), nil +} + +func toDeploymentSettingsFragment(minimal *gqlclient.DeploymentSettingsMinimalFragment) *gqlclient.DeploymentSettingsFragment { + if minimal == nil { + return nil + } + + return &gqlclient.DeploymentSettingsFragment{ + AgentHelmValues: minimal.AgentHelmValues, + AgentVsn: minimal.AgentVsn, + } +} + +func IsUnknownGraphQLField(err error, field string) bool { + if err == nil { + return false + } + + msg := err.Error() + quotedField := `"` + field + `"` + return strings.Contains(msg, "Cannot query field") && strings.Contains(msg, quotedField) +} From e6bda49e76756f9772985eaec747549f7d7cdbe8 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 15 Jul 2026 15:03:14 +0200 Subject: [PATCH 2/5] Update pkg/console/agent_helm_values.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- pkg/console/agent_helm_values.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/console/agent_helm_values.go b/pkg/console/agent_helm_values.go index ea4620758..c50c3032d 100644 --- a/pkg/console/agent_helm_values.go +++ b/pkg/console/agent_helm_values.go @@ -64,8 +64,12 @@ func clusterBindings(cluster *gqlclient.ClusterFragment) map[string]interface{} "Tags": clusterTagsMap(cluster.Tags), } + lowercase := make(map[string]interface{}, len(res)) for k, v := range res { - res[strings.ToLower(k)] = v + lowercase[strings.ToLower(k)] = v + } + for k, v := range lowercase { + res[k] = v } res["kasUrl"] = cluster.KasURL res["currentVersion"] = cluster.CurrentVersion From addd69bd5a3a9eaa1664a806dcf217f5fb327017 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 15 Jul 2026 15:04:14 +0200 Subject: [PATCH 3/5] gen mocks --- pkg/test/mocks/ConsoleClient.go | 30 ++++++++++++++++++++++++++++++ pkg/test/mocks/Kube.go | 7 +++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/test/mocks/ConsoleClient.go b/pkg/test/mocks/ConsoleClient.go index 0474a856d..3ba2c578b 100644 --- a/pkg/test/mocks/ConsoleClient.go +++ b/pkg/test/mocks/ConsoleClient.go @@ -631,6 +631,36 @@ func (_m *ConsoleClient) GetGlobalSettings() (*client.DeploymentSettingsFragment return r0, r1 } +// GetGlobalSettingsMinimal provides a mock function with no fields +func (_m *ConsoleClient) GetGlobalSettingsMinimal() (*client.DeploymentSettingsFragment, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetGlobalSettingsMinimal") + } + + var r0 *client.DeploymentSettingsFragment + var r1 error + if rf, ok := ret.Get(0).(func() (*client.DeploymentSettingsFragment, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *client.DeploymentSettingsFragment); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.DeploymentSettingsFragment) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetPipelineContext provides a mock function with given fields: id func (_m *ConsoleClient) GetPipelineContext(id string) (*client.PipelineContextFragment, error) { ret := _m.Called(id) diff --git a/pkg/test/mocks/Kube.go b/pkg/test/mocks/Kube.go index 646fdd350..aaa40637d 100644 --- a/pkg/test/mocks/Kube.go +++ b/pkg/test/mocks/Kube.go @@ -5,10 +5,13 @@ package mocks import ( client_gokubernetes "k8s.io/client-go/kubernetes" - mock "github.com/stretchr/testify/mock" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mock "github.com/stretchr/testify/mock" + rest "k8s.io/client-go/rest" + + v1 "k8s.io/api/core/v1" ) // Kube is an autogenerated mock type for the Kube type From 97e99a08610bc633f1800552553daf1e899282f1 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 15 Jul 2026 15:07:15 +0200 Subject: [PATCH 4/5] update error handling --- pkg/console/agent_helm_values_test.go | 10 ---------- pkg/console/settings.go | 25 +++---------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/pkg/console/agent_helm_values_test.go b/pkg/console/agent_helm_values_test.go index b761b3742..937482867 100644 --- a/pkg/console/agent_helm_values_test.go +++ b/pkg/console/agent_helm_values_test.go @@ -1,7 +1,6 @@ package console_test import ( - "errors" "testing" gqlclient "github.com/pluralsh/console/go/client" @@ -11,15 +10,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestIsUnknownGraphQLField(t *testing.T) { - t.Parallel() - - err := errors.New(`GraphQL error: GetDeploymentSettings: Cannot query field "agentHelmValuesTemplateable" on type "DeploymentSettings".`) - assert.True(t, console.IsUnknownGraphQLField(err, "agentHelmValuesTemplateable")) - assert.False(t, console.IsUnknownGraphQLField(err, "agentHelmValues")) - assert.False(t, console.IsUnknownGraphQLField(nil, "agentHelmValuesTemplateable")) -} - func TestResolveAgentHelmValuesWithoutTemplating(t *testing.T) { t.Parallel() diff --git a/pkg/console/settings.go b/pkg/console/settings.go index 776ce436d..8ae3b6b05 100644 --- a/pkg/console/settings.go +++ b/pkg/console/settings.go @@ -2,7 +2,6 @@ package console import ( "fmt" - "strings" gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/api" @@ -21,20 +20,12 @@ func (c *consoleClient) UpdateDeploymentSettings(attr gqlclient.DeploymentSettin } func (c *consoleClient) GetGlobalSettings() (*gqlclient.DeploymentSettingsFragment, error) { - settings, err := c.getGlobalSettings() - if err != nil && IsUnknownGraphQLField(err, "agentHelmValuesTemplateable") { - return c.GetGlobalSettingsMinimal() - } - return settings, err -} - -func (c *consoleClient) getGlobalSettings() (*gqlclient.DeploymentSettingsFragment, error) { resp, err := c.client.GetDeploymentSettings(c.ctx) if err != nil { - return nil, api.GetErrorResponse(err, "GetDeploymentSettings") + return c.GetGlobalSettingsMinimal() } - if resp == nil { - return nil, fmt.Errorf("returned GetDeploymentSettings object is nil") + if resp == nil || resp.DeploymentSettings == nil { + return c.GetGlobalSettingsMinimal() } return resp.DeploymentSettings, nil } @@ -60,13 +51,3 @@ func toDeploymentSettingsFragment(minimal *gqlclient.DeploymentSettingsMinimalFr AgentVsn: minimal.AgentVsn, } } - -func IsUnknownGraphQLField(err error, field string) bool { - if err == nil { - return false - } - - msg := err.Error() - quotedField := `"` + field + `"` - return strings.Contains(msg, "Cannot query field") && strings.Contains(msg, quotedField) -} From c6c0d740ac6dd0c21628d6116c0347675298b1ce Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 15 Jul 2026 15:28:09 +0200 Subject: [PATCH 5/5] linter --- pkg/client/plural.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/plural.go b/pkg/client/plural.go index 470d03e00..b244a1e81 100644 --- a/pkg/client/plural.go +++ b/pkg/client/plural.go @@ -7,8 +7,8 @@ import ( "os/exec" "strings" - "github.com/pluralsh/console/go/polly/algorithms" consoleclient "github.com/pluralsh/console/go/client" + "github.com/pluralsh/console/go/polly/algorithms" "github.com/samber/lo" apierrors "k8s.io/apimachinery/pkg/api/errors"