Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion apps/workspace-engine/pkg/oapi/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (lv LiteralValue) String() string {

type TemplatableRelease struct {
Release
Variables map[string]string
Variables map[string]string `json:"variables"`
}

func (r *Release) ToTemplatable() (*TemplatableRelease, error) {
Expand All @@ -32,6 +32,33 @@ func (r *Release) ToTemplatable() (*TemplatableRelease, error) {
}, nil
}

// TemplatableJobData is the data structure used for Go templating.
// It provides a consistent lowercase field naming convention for templates.
// Fields use JSON tags to ensure consistent key naming when converted to map.
type TemplatableJobData struct {
Job Job `json:"job"`
Release *TemplatableRelease `json:"release"`
Resource *Resource `json:"resource"`
Environment *Environment `json:"environment"`
Deployment *Deployment `json:"deployment"`

mapCache map[string]any `json:"-"`
}

// Map converts the TemplatableJobData to a map[string]any using JSON marshaling.
// This ensures consistent lowercase/camelCase key names matching JSON tags.
// The result is cached for performance.
func (t *TemplatableJobData) Map() map[string]any {
if t.mapCache != nil {
return t.mapCache
}
data, _ := json.Marshal(t)
var result map[string]any
_ = json.Unmarshal(data, &result)
t.mapCache = result
return t.mapCache
}

type TemplatableJob struct {
JobWithRelease
Release *TemplatableRelease
Expand All @@ -48,3 +75,17 @@ func (j *JobWithRelease) ToTemplatable() (*TemplatableJob,
Release: release,
}, nil
}

// ToTemplateData converts TemplatableJob to TemplatableJobData for use in templates.
// The returned map uses lowercase/camelCase keys matching the JSON tags,
// providing consistent template variable naming (e.g., {{.resource.name}} instead of {{.Resource.Name}}).
func (t *TemplatableJob) ToTemplateData() map[string]any {
data := &TemplatableJobData{
Job: t.Job,
Release: t.Release,
Resource: t.Resource,
Environment: t.Environment,
Deployment: t.Deployment,
}
return data.Map()
}
5 changes: 4 additions & 1 deletion apps/workspace-engine/pkg/workspace/jobdispatch/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,11 @@ func (d *ArgoCDDispatcher) DispatchJob(ctx context.Context, job *oapi.Job) error
return fmt.Errorf("failed to parse template: %w", err)
}

// Convert to map with lowercase keys for consistent template variable naming
templateData := templatableJobWithRelease.ToTemplateData()

var buf bytes.Buffer
if err := t.Execute(&buf, templatableJobWithRelease); err != nil {
if err := t.Execute(&buf, templateData); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute template")
message := fmt.Sprintf("Failed to execute ArgoCD Application template: %s", err.Error())
Expand Down
68 changes: 35 additions & 33 deletions apps/workspace-engine/pkg/workspace/jobdispatch/argocd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,10 @@ func executeTemplate(templateStr string, data *oapi.TemplatableJob) (string, err
if err != nil {
return "", err
}
// Convert to map with lowercase keys for consistent template variable naming
templateData := data.ToTemplateData()
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
if err := t.Execute(&buf, templateData); err != nil {
return "", err
}
return buf.String(), nil
Expand Down Expand Up @@ -391,32 +393,32 @@ func TestTemplateExecution_BasicFields(t *testing.T) {
}{
{
name: "job id",
template: `{{ .Job.Id }}`,
template: `{{ .job.id }}`,
expected: "job-123",
},
{
name: "resource name",
template: `{{ .Resource.Name }}`,
template: `{{ .resource.name }}`,
expected: "my-app",
},
{
name: "resource identifier",
template: `{{ .Resource.Identifier }}`,
template: `{{ .resource.identifier }}`,
expected: "my-app-identifier",
},
{
name: "environment name",
template: `{{ .Environment.Name }}`,
template: `{{ .environment.name }}`,
expected: "production",
},
{
name: "deployment name",
template: `{{ .Deployment.Name }}`,
template: `{{ .deployment.name }}`,
expected: "my-deployment",
},
{
name: "release version name",
template: `{{ .Release.Version.Name }}`,
template: `{{ .release.version.name }}`,
expected: "v1.2.3",
},
}
Expand All @@ -440,17 +442,17 @@ func TestTemplateExecution_ReleaseVariables(t *testing.T) {
}{
{
name: "access variable by key",
template: `{{ index .Release.Variables "IMAGE_TAG" }}`,
template: `{{ index .release.variables "IMAGE_TAG" }}`,
expected: "v1.2.3",
},
{
name: "access multiple variables",
template: `tag={{ index .Release.Variables "IMAGE_TAG" }}, replicas={{ index .Release.Variables "REPLICAS" }}`,
template: `tag={{ index .release.variables "IMAGE_TAG" }}, replicas={{ index .release.variables "REPLICAS" }}`,
expected: "tag=v1.2.3, replicas=3",
},
{
name: "missing variable returns empty with missingkey=zero",
template: `{{ index .Release.Variables "NONEXISTENT" }}`,
name: "missing variable returns no value (use default function to handle)",
template: `{{ index .release.variables "NONEXISTENT" | default "" }}`,
expected: "",
},
}
Expand All @@ -474,12 +476,12 @@ func TestTemplateExecution_ResourceConfig(t *testing.T) {
}{
{
name: "access config value",
template: `{{ index .Resource.Config "namespace" }}`,
template: `{{ index .resource.config "namespace" }}`,
expected: "production",
},
{
name: "access nested config",
template: `{{ index .Resource.Config "cluster" }}`,
template: `{{ index .resource.config "cluster" }}`,
expected: "us-west-2",
},
}
Expand All @@ -503,22 +505,22 @@ func TestTemplateExecution_SprigFunctions(t *testing.T) {
}{
{
name: "lower function",
template: `{{ .Resource.Name | lower }}`,
template: `{{ .resource.name | lower }}`,
expected: "my-app",
},
{
name: "upper function",
template: `{{ .Resource.Name | upper }}`,
template: `{{ .resource.name | upper }}`,
expected: "MY-APP",
},
{
name: "replace function",
template: `{{ .Resource.Name | replace "-" "_" }}`,
template: `{{ .resource.name | replace "-" "_" }}`,
expected: "my_app",
},
{
name: "default function for missing value",
template: `{{ index .Release.Variables "MISSING" | default "default-value" }}`,
template: `{{ index .release.variables "MISSING" | default "default-value" }}`,
expected: "default-value",
},
}
Expand All @@ -538,20 +540,20 @@ func TestTemplateExecution_FullArgoCDApplication(t *testing.T) {
templateStr := `apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ .Resource.Name }}
name: {{ .resource.name }}
namespace: argocd
labels:
app: {{ .Resource.Name }}
env: {{ .Environment.Name }}
app: {{ .resource.name }}
env: {{ .environment.name }}
spec:
project: default
source:
repoURL: https://github.com/example/repo
path: manifests/{{ .Environment.Name }}
targetRevision: {{ index .Release.Variables "IMAGE_TAG" }}
path: manifests/{{ .environment.name }}
targetRevision: {{ index .release.variables "IMAGE_TAG" }}
destination:
server: https://kubernetes.default.svc
namespace: {{ index .Resource.Config "namespace" }}`
namespace: {{ index .resource.config "namespace" }}`

result, err := executeTemplate(templateStr, job)
require.NoError(t, err)
Expand All @@ -578,7 +580,7 @@ func TestTemplateExecution_NilResource(t *testing.T) {
job.Resource = nil

// With missingkey=zero, accessing nil resource should not panic
templateStr := `name: {{ if .Resource }}{{ .Resource.Name }}{{ else }}unknown{{ end }}`
templateStr := `name: {{ if .resource }}{{ .resource.name }}{{ else }}unknown{{ end }}`
result, err := executeTemplate(templateStr, job)
require.NoError(t, err)
require.Equal(t, "name: unknown", result)
Expand All @@ -593,11 +595,11 @@ func TestTemplateExecution_InvalidTemplate(t *testing.T) {
}{
{
name: "unclosed action",
template: `{{ .Job.Id`,
template: `{{ .job.id`,
},
{
name: "unknown function",
template: `{{ nonexistentFunc .Job.Id }}`,
template: `{{ nonexistentFunc .job.id }}`,
},
}

Expand Down Expand Up @@ -787,15 +789,15 @@ func TestArgoCDDispatcher_DispatchJob_Success(t *testing.T) {
templateStr := `apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ .Resource.Name }}
name: {{ .resource.name }}
namespace: argocd
labels:
env: {{ .Environment.Name }}
env: {{ .environment.name }}
spec:
project: default
source:
repoURL: https://github.com/example/repo
targetRevision: {{ .Release.Version.Name }}
targetRevision: {{ .release.version.name }}
destination:
server: https://kubernetes.default.svc
namespace: production`
Expand Down Expand Up @@ -862,10 +864,10 @@ func TestArgoCDDispatcher_DispatchJob_CleansApplicationName(t *testing.T) {
templateStr := `apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {{ .Resource.Name }}
name: {{ .resource.name }}
namespace: argocd
labels:
original-name: {{ .Resource.Name }}`
original-name: {{ .resource.name }}`

config := createArgoCDJobConfig(t, templateStr)

Expand Down Expand Up @@ -984,7 +986,7 @@ func TestArgoCDDispatcher_DispatchJob_InvalidTemplate(t *testing.T) {
)

// Invalid template syntax
templateStr := `{{ .Resource.Name`
templateStr := `{{ .resource.name`

config := createArgoCDJobConfig(t, templateStr)

Expand Down Expand Up @@ -1202,7 +1204,7 @@ func TestArgoCDDispatcher_DispatchJob_InvalidTemplate_SendsMessage(t *testing.T)
)

// Invalid template syntax
templateStr := `{{ .Resource.Name`
templateStr := `{{ .resource.name`

config := createArgoCDJobConfig(t, templateStr)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,11 @@ func (d *TerraformCloudDispatcher) generateWorkspace(job *oapi.TemplatableJob, t
return nil, fmt.Errorf("failed to parse template: %w", err)
}

// Convert to map with lowercase keys for consistent template variable naming
templateData := job.ToTemplateData()

var buf bytes.Buffer
if err := t.Execute(&buf, job); err != nil {
if err := t.Execute(&buf, templateData); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}

Expand Down
Loading