Skip to content
Merged
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
18 changes: 18 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ tasks:
deps: [check:env]
cmds:
- task: setup:cluster
- task: setup:coredns
- task: setup:tekton
- task: setup:tekton-pruner
- task: setup:argocd
Expand Down Expand Up @@ -93,6 +94,21 @@ tasks:
- kubectl config set-cluster k3d-{{.CLUSTER_NAME}} --server=https://localhost:6550
- kubectl cluster-info

setup:coredns:
desc: Patch CoreDNS to use robust external DNS servers instead of k3d node's resolv.conf
cmds:
- echo "Patching CoreDNS to use 8.8.8.8 and 1.1.1.1..."
- cmd: |
kubectl get configmap coredns -n kube-system -o yaml > coredns.yaml
sed 's/forward . \/etc\/resolv.conf/forward . 8.8.8.8 1.1.1.1/' coredns.yaml > coredns-patched.yaml
kubectl apply -f coredns-patched.yaml
rm coredns.yaml coredns-patched.yaml
platforms: [linux, darwin]
- cmd: |
powershell -ExecutionPolicy Bypass -Command "$coredns = kubectl get configmap coredns -n kube-system -o yaml; $coredns = $coredns -replace 'forward \. /etc/resolv\.conf', 'forward . 8.8.8.8 1.1.1.1'; $coredns | Out-File -Encoding utf8 coredns.yaml; kubectl apply -f coredns.yaml; Remove-Item coredns.yaml"
platforms: [windows]
- kubectl rollout restart -n kube-system deployment/coredns

setup:tekton:
desc: Install Tekton Pipeline, Triggers, and Interceptors
status:
Expand Down Expand Up @@ -217,6 +233,8 @@ tasks:
--set postgresql-ha.enabled=false
--set redis-cluster.enabled=false
--set redis.enabled=false
--set valkey-cluster.enabled=false
--set valkey.enabled=false
--set gitea.config.database.DB_TYPE=sqlite3
--set gitea.config.session.PROVIDER=memory
--set gitea.config.cache.ADAPTER=memory
Expand Down
4 changes: 2 additions & 2 deletions apps/operator/api/v1alpha1/heliosapp_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ type HeliosAppSpec struct {
// +optional
ContextSubpath string `json:"contextSubpath,omitempty"`

// DatabaseSecretRef is the name of the secret containing database credentials for migrations
// DatabaseSecretRef is the name of the secret containing database credentials for migrations.
// Defaults to {appName}-db-secret if not set.
// +optional
// +kubebuilder:default="api-db-secret"
DatabaseSecretRef string `json:"databaseSecretRef,omitempty"`

// Components define the workloads of the application
Expand Down
5 changes: 5 additions & 0 deletions apps/operator/config/crd/bases/app.helios.io_heliosapps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ spec:
contextSubpath:
description: ContextSubpath is the path where the Dockerfile is located
type: string
databaseSecretRef:
description: |-
DatabaseSecretRef is the name of the secret containing database credentials for migrations.
Defaults to {appName}-db-secret if not set.
type: string
description:
description: Description of the application
type: string
Expand Down
4 changes: 3 additions & 1 deletion apps/operator/internal/controller/database/injection.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const (
dbPortEnvName = "DB_PORT"
dbNameEnvName = "DB_NAME"
databaseURLEnvName = "DATABASE_URL"
// dbTypePostgres is the canonical DB type identifier used across the database package.
dbTypePostgres = "postgres"
// postgresDatabaseURLTemplate uses Kubernetes $(VAR) expansion from earlier env entries.
postgresDatabaseURLTemplate = "postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
)
Expand All @@ -20,7 +22,7 @@ const (
// Only types with a defined URL layout are supported; others return ok=false.
func connectionURLTemplateForDBType(dbType string) (template string, ok bool) {
switch strings.ToLower(strings.TrimSpace(dbType)) {
case "postgres", "postgresql":
case dbTypePostgres, "postgresql":
return postgresDatabaseURLTemplate, true
default:
return "", false
Expand Down
6 changes: 3 additions & 3 deletions apps/operator/internal/controller/database/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (r *Reconciler) ReconcileSecrets(ctx context.Context, app *appv1alpha1.Heli
}

for _, dbTrait := range dbTraits {
if strings.ToLower(dbTrait.Properties.DBType) != "postgres" {
if strings.ToLower(dbTrait.Properties.DBType) != dbTypePostgres {
log.V(1).Info("Skipping credential secret creation for non-postgres database type",
"component", dbTrait.ComponentName,
"dbType", dbTrait.Properties.DBType)
Expand Down Expand Up @@ -146,7 +146,7 @@ func (r *Reconciler) ReconcileInstances(ctx context.Context, app *appv1alpha1.He
}

for _, dbTrait := range dbTraits {
if strings.ToLower(dbTrait.Properties.DBType) != "postgres" {
if strings.ToLower(dbTrait.Properties.DBType) != dbTypePostgres {
log.V(1).Info("Skipping non-postgres database type",
"component", dbTrait.ComponentName,
"dbType", dbTrait.Properties.DBType)
Expand Down Expand Up @@ -308,7 +308,7 @@ func (r *Reconciler) ReconcileInjection(ctx context.Context, app *appv1alpha1.He
pendingInjection := false

for _, dbTrait := range dbTraits {
if strings.ToLower(dbTrait.Properties.DBType) != "postgres" {
if strings.ToLower(dbTrait.Properties.DBType) != dbTypePostgres {
log.V(1).Info("Skipping env var injection for non-postgres database type",
"component", dbTrait.ComponentName,
"dbType", dbTrait.Properties.DBType)
Expand Down
6 changes: 3 additions & 3 deletions apps/operator/internal/controller/database/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, s
"app": name,
"helios.io/managed-by": "operator",
"helios.io/trait": "database",
"helios.io/db-type": "postgres",
"helios.io/db-type": dbTypePostgres,
}

probeCommand := `pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -p "$PGPORT"`
Expand All @@ -119,12 +119,12 @@ func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, s
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "postgres",
Name: dbTypePostgres,
Image: fmt.Sprintf("postgres:%s", version),
Ports: []corev1.ContainerPort{
{
ContainerPort: port,
Name: "postgres",
Name: dbTypePostgres,
},
},
Env: []corev1.EnvVar{
Expand Down
10 changes: 5 additions & 5 deletions apps/operator/internal/controller/database/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestFormatPostgresURI(t *testing.T) {
host: "localhost",
dbName: "mydb",
port: 5432,
expected: "postgres://user:pass@localhost:5432/mydb",
expected: "postgres://user:pass@localhost:5432/mydb?sslmode=disable",
},
{
name: "special chars in password",
Expand All @@ -38,7 +38,7 @@ func TestFormatPostgresURI(t *testing.T) {
host: "db.example.com",
dbName: "app_db",
port: 5432,
expected: "postgres://user:p%40ss%3Aword%2F@db.example.com:5432/app_db",
expected: "postgres://user:p%40ss%3Aword%2F@db.example.com:5432/app_db?sslmode=disable",
},
{
name: "special chars in username",
Expand All @@ -47,7 +47,7 @@ func TestFormatPostgresURI(t *testing.T) {
host: "postgres-host",
dbName: "database",
port: 5432,
expected: "postgres://user%2Badmin:password@postgres-host:5432/database",
expected: "postgres://user%2Badmin:password@postgres-host:5432/database?sslmode=disable",
},
{
name: "custom port",
Expand All @@ -56,7 +56,7 @@ func TestFormatPostgresURI(t *testing.T) {
host: "db-instance",
dbName: "prod_db",
port: 5433,
expected: "postgres://admin:secret123@db-instance:5433/prod_db",
expected: "postgres://admin:secret123@db-instance:5433/prod_db?sslmode=disable",
},
{
name: "IPv6 host",
Expand All @@ -65,7 +65,7 @@ func TestFormatPostgresURI(t *testing.T) {
host: "::1",
dbName: "testdb",
port: 5432,
expected: "postgres://user:pass@::1:5432/testdb",
expected: "postgres://user:pass@::1:5432/testdb?sslmode=disable",
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IPv6 test case (and current formatPostgresURI behavior) produces postgres://...@::1:5432/..., which is not a valid URI because IPv6 hosts must be wrapped in brackets ([::1]). Consider updating formatPostgresURI to bracket IPv6 literals and adjust this expectation accordingly.

Suggested change
expected: "postgres://user:pass@::1:5432/testdb?sslmode=disable",
expected: "postgres://user:pass@[::1]:5432/testdb?sslmode=disable",

Copilot uses AI. Check for mistakes.
},
}

Expand Down
29 changes: 16 additions & 13 deletions apps/operator/internal/controller/tekton/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,29 @@ func MapCRDToTektonInput(app *appv1alpha1.HeliosApp) cueModel.TektonInput {
PipelineName: app.Spec.PipelineName,
// PipelineType is intentionally derived from PipelineName because
// the HeliosApp CRD does not have a separate PipelineType field.
PipelineType: app.Spec.PipelineName,
TriggerType: app.Spec.TriggerType,
ServiceAccount: app.Spec.ServiceAccount,
PVCName: app.Spec.PVCName,
ContextSubpath: app.Spec.ContextSubpath,
Replicas: int(app.Spec.Replicas),
Port: int(app.Spec.Port),
TestCommand: app.Spec.TestCommand,
TestImage: app.Spec.TestImage,
DockerSecret: "docker-credentials",
PipelineType: app.Spec.PipelineName,
TriggerType: app.Spec.TriggerType,
ServiceAccount: app.Spec.ServiceAccount,
PVCName: app.Spec.PVCName,
ContextSubpath: app.Spec.ContextSubpath,
Replicas: int(app.Spec.Replicas),
Port: int(app.Spec.Port),
TestCommand: app.Spec.TestCommand,
TestImage: app.Spec.TestImage,
DockerSecret: "docker-credentials",
DatabaseSecretRef: app.Spec.DatabaseSecretRef,
ArgoCDNamespace: app.Spec.ArgoCDNamespace,
ArgoCDProject: app.Spec.ArgoCDProject,
ArgoCDNamespace: app.Spec.ArgoCDNamespace,
ArgoCDProject: app.Spec.ArgoCDProject,
}

input.GitBranch = cmp.Or(input.GitBranch, "main")
input.GitOpsBranch = cmp.Or(input.GitOpsBranch, "main")
input.GitOpsSecretRef = cmp.Or(input.GitOpsSecretRef, "helios-gitops-bot")
input.WebhookSecret = cmp.Or(input.WebhookSecret, "gitea-webhook-secret")
input.DatabaseSecretRef = cmp.Or(input.DatabaseSecretRef, "api-db-secret")
// Derive the database secret name dynamically from app name if not explicitly set
if input.DatabaseSecretRef == "" {
input.DatabaseSecretRef = app.Name + "-db-secret"
}
input.TriggerType = cmp.Or(input.TriggerType, "gitea-push")
if input.PipelineName == "" {
input.PipelineName = defaultPipelineName
Expand Down
9 changes: 0 additions & 9 deletions apps/operator/internal/controller/tekton/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,6 @@ func GeneratePipelineRun(heliosApp *appv1alpha1.HeliosApp, pipelineName string)
}
testImage := cmp.Or(heliosApp.Spec.TestImage, "node:24")

replicas := heliosApp.Spec.Replicas
if replicas <= 0 {
replicas = 1
}
port := heliosApp.Spec.Port
if port <= 0 || port > 65535 {
port = 8080
}

params := make([]any, 0, 18)
params = append(params,
map[string]any{"name": "app-repo-url", "value": shared.RewriteGiteaURL(heliosApp.Spec.GitRepo)},
Expand Down
33 changes: 17 additions & 16 deletions apps/operator/internal/cue/tekton_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ func TestRenderTektonResources_AllResources(t *testing.T) {
t.Fatalf("RenderTektonResources failed: %v", err)
}

// With webhookDomain set, we expect 11 objects:
// 6 Tasks (git-clone, kaniko-build, git-update-manifest, argocd-sync, db-migrate, postgrest-reload) + 1 Pipeline + 1 TriggerBinding + 1 TriggerTemplate + 1 EventListener + 1 Ingress
expectedCount := 11
// With webhookDomain set, we expect 13 objects:
// 6 Tasks (git-clone, kaniko-build, git-update-manifest, argocd-sync, db-migrate, postgrest-reload)
// + 2 Pipelines (from-code-to-cluster + db-migrate)
// + 1 TriggerBinding + 2 TriggerTemplates (gitea + db-migrate) + 1 EventListener + 1 Ingress
expectedCount := 13
if len(objects) != expectedCount {
t.Errorf("Expected %d objects, got %d", expectedCount, len(objects))
for i, obj := range objects {
Expand All @@ -80,9 +82,9 @@ func TestRenderTektonResources_AllResources(t *testing.T) {
// Verify each expected kind is present
expectedKinds := map[string]int{
"Task": 6,
"Pipeline": 1,
"Pipeline": 2,
"TriggerBinding": 1,
"TriggerTemplate": 1,
"TriggerTemplate": 2,
"EventListener": 1,
"Ingress": 1,
}
Expand Down Expand Up @@ -115,8 +117,9 @@ func TestRenderTektonResources_WithoutWebhook(t *testing.T) {
t.Fatalf("RenderTektonResources failed: %v", err)
}

// Without webhookDomain: 10 objects (no Ingress)
expectedCount := 10
// Without webhookDomain: 12 objects (no Ingress)
// 6 Tasks + 2 Pipelines + 1 TriggerBinding + 2 TriggerTemplates + 1 EventListener
expectedCount := 12
if len(objects) != expectedCount {
t.Errorf("Expected %d objects (no webhook), got %d", expectedCount, len(objects))
for i, obj := range objects {
Expand Down Expand Up @@ -255,17 +258,15 @@ func TestRenderTektonResources_BuildOnlyPipeline(t *testing.T) {
t.Fatalf("RenderTektonResources failed: %v", err)
}

// Verify pipeline name is "build-only"
foundPipeline := false
// Verify the primary pipeline with name "build-only" exists among rendered pipelines.
// Note: db-migrate pipeline is always rendered alongside the primary pipeline.
foundBuildOnly := false
for _, obj := range objects {
if obj.GetKind() == "Pipeline" {
if obj.GetName() != "build-only" {
t.Errorf("Expected pipeline name 'build-only', got %q", obj.GetName())
}
foundPipeline = true
if obj.GetKind() == "Pipeline" && obj.GetName() == "build-only" {
foundBuildOnly = true
}
}
if !foundPipeline {
t.Error("Pipeline not found in rendered objects")
if !foundBuildOnly {
t.Error("Expected primary pipeline 'build-only' not found in rendered objects")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ metadata:
name: ${{ values.name }}
description: ${{ values.description | dump }}
annotations:
gitea.org/repo-url: http://localhost:3030/${{ values.owner }}/${{ values.name }}
backstage.io/techdocs-ref: dir:.
backstage.io/kubernetes-id: ${{ values.name }}
backstage.io/kubernetes-label-selector: app.kubernetes.io/name=${{ values.name }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spec:
triggerType: db-migrate

components:
- name: api
- name: ${{ values.name }}
type: web-service
properties:
# Use official PostgREST image from Docker Hub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ kind: Component
metadata:
name: ${{ values.name }}
annotations:
backstage.io/techdocs-ref: dir:.
backstage.io/kubernetes-id: ${{ values.name }}
backstage.io/kubernetes-label-selector: app.kubernetes.io/name=${{ values.name }}
backstage.io/kubernetes-namespace: default
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backstage.io/kubernetes-namespace is hardcoded to default, but this template lets the user choose a namespace (parameters.namespace). This will break Kubernetes/CI-CD tab discovery when deploying outside default; consider templating this annotation (e.g. via values.namespace) and passing the namespace into the source fetch:template values.

Suggested change
backstage.io/kubernetes-namespace: default
backstage.io/kubernetes-namespace: ${{ values.namespace }}

Copilot uses AI. Check for mistakes.
janus-idp.io/tekton: ${{ values.name }}
tekton.dev/ci-cd: "true"
argocd/app-name: ${{ values.name }}-argocd
spec:
type: service
Expand Down
9 changes: 6 additions & 3 deletions apps/portal/examples/postgrest-template/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ spec:
jwtSecret:
title: JWT Secret
type: string
description: Secret for signing JWT tokens (at least 32 chars recommended)
description: Secret for signing JWT tokens (32-64 characters required for PostgREST)
minLength: 32
maxLength: 64
ui:field: Secret
jwtRole:
title: JWT Role
type: string
Expand Down Expand Up @@ -111,7 +114,7 @@ spec:
description: "PostgREST API: ${{ parameters.name }}"
image: index.docker.io/${{ parameters.dockerOrg }}/${{ parameters.repoName }}
apiSchema: ${{ parameters.apiSchema }}
jwtSecret: ${{ parameters.jwtSecret }}
jwtSecret: ${{ secrets.jwtSecret }}
jwtRole: ${{ parameters.jwtRole }}
Comment on lines 114 to 118
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${{ secrets.jwtSecret }} is unlikely to resolve here: the portal scaffolder config only defines environment secrets like GITEA_TOKEN, so this will render an empty JWT secret (or fail templating). Use the user input (parameters.jwtSecret) and rely on ui:field: Secret/password masking to avoid leaking it in the UI/logs.

Copilot uses AI. Check for mistakes.
anonRole: ${{ parameters.anonRole }}

Expand Down Expand Up @@ -152,7 +155,7 @@ spec:
databaseName: ${{ parameters.databaseConfig.dbName }}
databaseVersion: ${{ parameters.databaseConfig.version or '16' }}
apiSchema: ${{ parameters.apiSchema }}
jwtSecret: ${{ parameters.jwtSecret }}
jwtSecret: ${{ secrets.jwtSecret }}
jwtRole: ${{ parameters.jwtRole }}
Comment on lines 156 to 159
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as earlier: ${{ secrets.jwtSecret }} is not provided by the scaffolder execution context (only configured env secrets like GITEA_TOKEN exist), so this will not propagate the JWT secret into the rendered GitOps manifests. Reference the parameter value instead.

Copilot uses AI. Check for mistakes.
anonRole: ${{ parameters.anonRole }}
owner: ${{ user.entity.metadata.name or 'guest' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ metadata:
name: ${{ values.name }}
description: ${{ values.description | dump }}
annotations:
gitea.org/repo-url: http://localhost:3030/${{ values.owner }}/${{ values.name }}
backstage.io/techdocs-ref: dir:.
backstage.io/kubernetes-id: ${{ values.name }}
backstage.io/kubernetes-label-selector: app.kubernetes.io/name=${{ values.name }}
Expand Down
1 change: 1 addition & 0 deletions apps/portal/examples/spring-boot-template/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ spec:
sourceRepo: ${{ steps['publish-source'].output.remoteUrl }}
gitopsRepo: ${{ steps['publish-source'].output.remoteUrl | replace(".git", "") }}-gitops
testCommand: "gradle test"
testImage: "gradle:8.7-jdk21"

- id: publish-gitops
name: Publish GitOps Manifests
Expand Down
9 changes: 6 additions & 3 deletions cue/definitions/tekton/triggers/db-migrate-trigger.cue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ import (
apiVersion: "tekton.dev/v1beta1"
kind: "PipelineRun"
metadata: {
name: "\(_bp.appName)-migrate-$(uid)"
// Truncate appName to at most 32 chars to keep total name ≤63 chars.
// CUE slice [:n] panics if string is shorter than n, so we guard with a conditional.
let _namePrefix = [if len(_bp.appName) > 32 {_bp.appName[:32]}, _bp.appName][0]
name: "\(_namePrefix)-migrate-$(uid)"
namespace: _bp.namespace
labels: {
"helios.io/managed-by": "helios-operator"
Expand All @@ -66,8 +69,8 @@ import (
params: [
{name: "app-repo-url", value: "$(tt.params.git-repo-url)"},
{name: "app-repo-revision", value: "$(tt.params.git-revision)"},
{name: "db-secret-name", value: "api-db-secret"},
{name: "migration-source", value: "db/migration"},
{name: "db-secret-name", value: _bp.databaseSecretRef},
{name: "migration-source", value: "db/migrations"},
{name: "namespace", value: _bp.namespace},
]
Comment on lines 69 to 75
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trigger now sets migration-source to db/migrations, but several file comments still refer to db/migration (singular) and the listener section says it filters by db/migration. Please update the comments/docs in this file to reflect the current behavior (and/or explicitly call out that the CEL filter supports both paths).

Copilot uses AI. Check for mistakes.

Expand Down
Loading
Loading