Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ GITEA_ADMIN_PASS=helios123
# -----------------------------------------------------------------------------
DOCKER_SERVER=https://index.docker.io/v1/
DOCKER_USERNAME=
DOCKER_PASSWORD=
DOCKER_EMAIL=
DOCKER_TOKEN=

# -----------------------------------------------------------------------------
# Git Author (optional, used by operator for GitOps commits)
Expand Down
11 changes: 5 additions & 6 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,24 +391,23 @@ tasks:
desc: Create Docker registry secret and link to pipeline SA
cmds:
- cmd: |
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ]; then
echo "DOCKER_USERNAME and DOCKER_PASSWORD must be set in .env" >&2
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_TOKEN" ]; then
echo "DOCKER_USERNAME and DOCKER_TOKEN must be set in .env" >&2
exit 1
fi
platforms: [linux, darwin]
- cmd: |
powershell -Command "if ([string]::IsNullOrEmpty($env:DOCKER_USERNAME) -or [string]::IsNullOrEmpty($env:DOCKER_PASSWORD)) { Write-Error 'DOCKER_USERNAME and DOCKER_PASSWORD must be set in .env'; exit 1 }"
powershell -Command "if ([string]::IsNullOrEmpty($env:DOCKER_USERNAME) -or [string]::IsNullOrEmpty($env:DOCKER_TOKEN)) { Write-Error 'DOCKER_USERNAME and DOCKER_TOKEN must be set in .env'; exit 1 }"
platforms: [windows]
- cmd: |
kubectl create secret docker-registry docker-credentials \
--docker-server=${DOCKER_SERVER:-https://index.docker.io/v1/} \
--docker-username=$DOCKER_USERNAME \
--docker-password=$DOCKER_PASSWORD \
--docker-email=${DOCKER_EMAIL:-dev@helios.io} \
--docker-password=$DOCKER_TOKEN \
--dry-run=client -o yaml | kubectl apply -f -
platforms: [linux, darwin]
- cmd: |
powershell -Command "$server = if ([string]::IsNullOrEmpty($env:DOCKER_SERVER)) { 'https://index.docker.io/v1/' } else { $env:DOCKER_SERVER }; $email = if ([string]::IsNullOrEmpty($env:DOCKER_EMAIL)) { 'dev@helios.io' } else { $env:DOCKER_EMAIL }; kubectl create secret docker-registry docker-credentials --docker-server=$server --docker-username=$env:DOCKER_USERNAME --docker-password=$env:DOCKER_PASSWORD --docker-email=$email --dry-run=client -o yaml | kubectl apply -f -"
powershell -Command "$server = if ([string]::IsNullOrEmpty($env:DOCKER_SERVER)) { 'https://index.docker.io/v1/' } else { $env:DOCKER_SERVER }; kubectl create secret docker-registry docker-credentials --docker-server=$server --docker-username=$env:DOCKER_USERNAME --docker-password=$env:DOCKER_TOKEN --dry-run=client -o yaml | kubectl apply -f -"
platforms: [windows]
- cmd: |
if kubectl get sa pipeline >/dev/null 2>&1; then
Expand Down
6 changes: 3 additions & 3 deletions apps/operator/internal/controller/database/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ func TestReconcileInstances(t *testing.T) {
Replicas: func() *int32 { r := int32(1); return &r }(),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "postgres", Image: "postgres:15"}},
Containers: []corev1.Container{{Name: "postgres", Image: "postgres:18"}},
},
},
Comment on lines 349 to 353

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

ReconcileInstances uses the database trait version ("16" in this test app) to set the StatefulSet image, but this test expects postgres:18. Align the expected image (and the existingSts seed image) with the trait version, or update the trait version if the default is being bumped.

Copilot uses AI. Check for mistakes.
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{
Expand Down Expand Up @@ -379,8 +379,8 @@ func TestReconcileInstances(t *testing.T) {
if err != nil {
t.Fatalf("failed to get updated StatefulSet: %v", err)
}
if got := updatedSts.Spec.Template.Spec.Containers[0].Image; got != "postgres:16" {
t.Fatalf("expected image postgres:16, got %s", got)
if got := updatedSts.Spec.Template.Spec.Containers[0].Image; got != "postgres:18" {
t.Fatalf("expected image postgres:18, got %s", got)
}

updatedSvc := &corev1.Service{}
Expand Down
4 changes: 2 additions & 2 deletions apps/operator/internal/controller/database/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ func TestGenerateDatabaseStatefulSet(t *testing.T) {
}

container := containers[0]
if container.Image != "postgres:16" {
t.Errorf("Expected image %q, got %q", "postgres:16", container.Image)
if container.Image != "postgres:18" {
t.Errorf("Expected image %q, got %q", "postgres:18", container.Image)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

This test passes version "16" into GenerateDatabaseStatefulSet(...) but asserts the image is postgres:18. Since the image is generated from the provided version, this expectation will fail unless the input version (or the generator) is updated consistently.

Suggested change
if container.Image != "postgres:18" {
t.Errorf("Expected image %q, got %q", "postgres:18", container.Image)
if container.Image != "postgres:16" {
t.Errorf("Expected image %q, got %q", "postgres:16", container.Image)

Copilot uses AI. Check for mistakes.
}

if len(container.Ports) != 1 || container.Ports[0].ContainerPort != 5432 {
Expand Down
10 changes: 9 additions & 1 deletion apps/portal/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ backend:
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
cors:
origin: http://localhost:3000
origin:
- http://localhost:3000
- http://127.0.0.1:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
# This is for local development only, it is not recommended to use this in production
Expand Down Expand Up @@ -132,6 +134,12 @@ catalog:
rules:
- allow: [Template]

# .NET Web API Template
- type: file
target: ../../examples/dotnet-template/template.yaml
rules:
- allow: [Template]

## Uncomment these lines to add more example data
# - type: url
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
Expand Down
14 changes: 14 additions & 0 deletions apps/portal/examples/dotnet-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# .NET Web API Backstage Template

This template scaffolds an ASP.NET Core Web API that is ready for Helios GitOps deployment.

Generated source repository includes:

- ASP.NET Core Web API project targeting .NET 10
- Multi-stage Dockerfile
- docker-compose setup for local development with PostgreSQL
- Environment-driven database configuration using `DB_HOST`, `DB_USER`, `DB_PASSWORD`, and `DB_NAME`

Generated GitOps repository includes:

- HeliosApp manifest with web-service + database trait
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apiVersion: app.helios.io/v1alpha1
kind: HeliosApp
metadata:
name: ${{ values.name }}
namespace: default
spec:

description: "Managed by Helios Operator"
owner: ${{ values.owner }}

imageRepo: ${{ values.dockerOrg }}/${{ values.repoName }}
gitRepo: "${{ values.sourceRepo }}"
gitopsRepo: ${{ values.gitopsRepo }}
gitopsPath: "apps/${{ values.name }}"
port: ${{ values.port }}
replicas: 1
testCommand: ${{ values.testCommand }}

webhookSecret: "git-credentials-${{ values.name }}"
gitopsSecretRef: "git-credentials-${{ values.name }}"

components:
- name: ${{ values.name }}-backend
type: web-service
properties:
image: ${{ values.dockerOrg }}/${{ values.repoName }}:latest
port: ${{ values.port }}
replicas: 1
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
traits:
- type: service
properties:
port: ${{ values.port }}
targetPort: ${{ values.port }}
- type: database
properties:
dbType: ${{ values.dbType | dump }}
dbName: ${{ values.dbName | dump }}
version: ${{ values.dbVersion | dump }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bin/
obj/
.git/
.vscode/
*.user
*.suo
11 changes: 11 additions & 0 deletions apps/portal/examples/dotnet-template/content/source/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Database credentials (injected by Helios Operator)
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=${{ values.name }}-db

# Legacy compatibility fallback used by some templates/operators
DB_PASS=postgres

# Application
PORT=${{ values.port }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
obj/
.vs/
*.user
*.suo
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;

namespace DotNetApi.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
});
}
}

public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string? Summary { get; set; }
}
17 changes: 17 additions & 0 deletions apps/portal/examples/dotnet-template/content/source/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

COPY DotNetApi.csproj ./
RUN dotnet restore DotNetApi.csproj

COPY . .
RUN dotnet publish DotNetApi.csproj -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:${{ values.port }}
EXPOSE ${{ values.port }}

ENTRYPOINT ["dotnet", "DotNetApi.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
</Project>
46 changes: 46 additions & 0 deletions apps/portal/examples/dotnet-template/content/source/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost";
var dbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres";
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? Environment.GetEnvironmentVariable("DB_PASS")
?? "postgres";
var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "${{ values.name }}-db";

var connectionTemplate = builder.Configuration.GetConnectionString("DefaultConnection")
?? "Host={DB_HOST};Username={DB_USER};Password={DB_PASSWORD};Database={DB_NAME}";
var connectionString = connectionTemplate
.Replace("{DB_HOST}", dbHost, StringComparison.Ordinal)
.Replace("{DB_USER}", dbUser, StringComparison.Ordinal)
.Replace("{DB_PASSWORD}", dbPassword, StringComparison.Ordinal)
.Replace("{DB_NAME}", dbName, StringComparison.Ordinal);
builder.Configuration["ConnectionStrings:DefaultConnection"] = connectionString;

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.MapControllers();

app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.MapGet("/database/config", ([FromServices] IConfiguration config) =>
Results.Ok(new
{
Host = dbHost,
User = dbUser,
Database = dbName,
ConnectionString = config.GetConnectionString("DefaultConnection")
}));

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

The /database/config endpoint returns the resolved ConnectionString, which includes the database password. This leaks credentials to any caller and should be removed or restricted (e.g., only enabled in Development and never include secrets in responses).

Suggested change
}
app.UseHttpsRedirection();
app.MapControllers();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.MapGet("/database/config", ([FromServices] IConfiguration config) =>
Results.Ok(new
{
Host = dbHost,
User = dbUser,
Database = dbName,
ConnectionString = config.GetConnectionString("DefaultConnection")
}));
app.MapGet("/database/config", () =>
Results.Ok(new
{
Host = dbHost,
User = dbUser,
Database = dbName
}));
}
app.UseHttpsRedirection();
app.MapControllers();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));

Copilot uses AI. Check for mistakes.

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:${{ values.port }}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
24 changes: 24 additions & 0 deletions apps/portal/examples/dotnet-template/content/source/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ${{ values.name }} (.NET Web API)

This project is generated by the Helios Backstage .NET template.

## Local Development

1. Start API + PostgreSQL:

```bash
docker compose up --build
```

2. Open Swagger:

http://localhost:${{ values.port }}/swagger

## Environment Variables

- DB_HOST
- DB_USER
- DB_PASSWORD
- DB_NAME

The API builds `ConnectionStrings:DefaultConnection` at startup from these values.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host={DB_HOST};Username={DB_USER};Password={DB_PASSWORD};Database={DB_NAME}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: backstage.io/v1alpha1
kind: Component
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 }}
backstage.io/kubernetes-namespace: default
janus-idp.io/tekton: ${{ values.name }}
tekton.dev/ci-cd: "true"
argocd/app-name: ${{ values.name }}-argocd
spec:
type: service
lifecycle: production
owner: ${{ values.owner }}
Loading
Loading