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
28 changes: 28 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.DS_Store
**/.classpath
**/.dockerignore
**/.env*
**/.git
**/.github
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/*Dockerfile
**/docs
LICENSE
README.md
41 changes: 41 additions & 0 deletions .github/workflows/integration-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: Integration Test
"on":
workflow_dispatch:
pull_request:
branches:
- main
paths:
- "integration/**"
- "**/*.go"
push:
branches:
- main

permissions:
id-token: write
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
integration-test:
name: Run integration tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Just
uses: ./.github/actions/setup-workspace

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Run integration tests
working-directory: ./integration
run: |
just build
just test-all
33 changes: 26 additions & 7 deletions cli/internal/templating/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,33 @@ import (

"github.com/jgfranco17/hackstack/cli/internal/fileutils"
"github.com/jgfranco17/hackstack/cli/internal/logging"
"github.com/sirupsen/logrus"
)

const (
extensionTemplate = ".j2"
extensionRawCopy = ".copy"
)

// Engine is the entity responsible for rendering embedded template
// files with provided source data.
type Engine struct {
Files fs.FS
Data CLIProject
}

// NewEngine creates a new templating engine instance with the provided
// embedded files and source data.
func NewEngine(files fs.FS, data CLIProject) *Engine {
return &Engine{
Files: files,
Data: data,
}
}

// Render processes the embedded template files and writes the output to the specified directory.
// It walks through all files in the embedded FS, rendering templates and copying raw files as
// needed. The function returns an error if any step of the rendering process fails.
func (e *Engine) Render(ctx context.Context, outputPath string) error {
logger := logging.FromContext(ctx).WithField("module", "templating")

Expand All @@ -36,7 +49,7 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error {

walker := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walk error at %q: %w", path, err)
return fmt.Errorf("walk error at %s: %w", path, err)
}
if d.IsDir() {
return nil
Expand All @@ -46,16 +59,22 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error {

var work func() error
switch {
case strings.HasSuffix(path, ".j2"):
destPath = strings.TrimSuffix(destPath, ".j2")
case strings.HasSuffix(path, extensionTemplate):
destPath = strings.TrimSuffix(destPath, extensionTemplate)
work = func() error {
logger.WithField("file", path).Trace("Rendering from template")
logger.WithFields(logrus.Fields{
"source": path,
"destination": destPath,
}).Trace("Rendering from template")
return renderTemplate(e.Files, path, destPath, e.Data)
}
case strings.HasSuffix(path, ".copy"):
destPath = strings.TrimSuffix(destPath, ".copy")
case strings.HasSuffix(path, extensionRawCopy):
destPath = strings.TrimSuffix(destPath, extensionRawCopy)
work = func() error {
logger.WithField("file", path).Trace("Copying file")
logger.WithFields(logrus.Fields{
"source": path,
"destination": destPath,
}).Trace("Copying raw file")
return fileutils.CopyFile(e.Files, path, destPath)
}
default:
Expand Down
File renamed without changes.
45 changes: 45 additions & 0 deletions integration/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ========== CLI BUILD STAGE ==========
ARG GO_VERSION=1.25
FROM golang:${GO_VERSION}-alpine AS build

WORKDIR /src
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod/ \
CGO_ENABLED=0 go build -o /bin/hackstack .

# ========== CLI SETUP STAGE ==========
FROM ubuntu:22.04 AS env-setup
SHELL ["/bin/bash", "-c"]

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates xz-utils && \
rm -rf /var/lib/apt/lists/*

COPY --from=build /bin/hackstack /bin/hackstack

# Tests should use non-root to simulate a real user.
ARG USERNAME=testuser
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN addgroup --gid $GROUP_ID ${USERNAME} && \
adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ${USERNAME}

FROM env-setup AS workspace-setup
USER ${USERNAME}
ENV HOME=/home/${USERNAME}
ARG PROJECT_DIR="${HOME}/projects"
WORKDIR ${PROJECT_DIR}

COPY examples/ ./examples/
RUN mkdir -p "${PROJECT_DIR}/backend" "${PROJECT_DIR}/cli"

# ========== APP RUN STAGE ==========
FROM workspace-setup AS app
SHELL ["/usr/bin/env", "bash", "-c"]

ENTRYPOINT [ "/bin/hackstack" ]
CMD ["--help"]
8 changes: 8 additions & 0 deletions integration/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Integration test Docker suite
services:
hackstack:
container_name: hackstack
image: hackstack:latest
build:
context: ..
dockerfile: integration/Dockerfile
55 changes: 55 additions & 0 deletions integration/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# INTEGRATION TEST SCRIPTS

CAPTURE_OUTPUT_FILE := "output.txt"

# Default target to list available commands
_default:
@just --list --unsorted

##########################################################
# TEST WORKSPACE SETUP
##########################################################

# Build the services in Docker
build:
docker compose build

# Workspace teardown
clean:
-docker compose down
-rm {{ CAPTURE_OUTPUT_FILE }}

##########################################################
# INTERACTIVE COMMANDS
##########################################################

# Run the CLI image with envs
hackstack *args:
@docker compose run \
--remove-orphans --rm \
hackstack {{ args }}

##########################################################
# TEST EXECUTION
##########################################################

# Run the full test suite
test-all: test-base test-build
@echo "Hackstack CLI integration test completed!"

# Base tests to verify the CLI is working and accessible
test-base:
#!/usr/bin/env bash
just hackstack --version | grep -n "hackstack"
just hackstack --help 2>&1 | grep -n "hackstack"

# Test the build command with a sample datasource
test-build:
#!/usr/bin/env bash
just hackstack build --help
just hackstack -vvv build backend \
--output "/home/testuser/projects/backend" \
--source examples/datasource.yaml
just hackstack -vvv build cli \
--output "/home/testuser/projects/cli" \
--source examples/datasource.yaml
Loading