diff --git a/.github/workflows/commitlint.yaml b/.github/workflows/commitlint.yaml index 9c359df..d8a7e90 100644 --- a/.github/workflows/commitlint.yaml +++ b/.github/workflows/commitlint.yaml @@ -12,4 +12,4 @@ on: jobs: validate: - uses: defenseunicorns/uds-common/.github/workflows/callable-commitlint.yaml@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/workflows/callable-commitlint.yaml@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a505931..98f9d17 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,5 +12,5 @@ on: jobs: validate: - uses: defenseunicorns/uds-common/.github/workflows/callable-lint.yaml@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/workflows/callable-lint.yaml@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 secrets: inherit diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e5ab3d3..4f49a1d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,7 +26,7 @@ jobs: matrix: flavor: [upstream, unicorn] architecture: [amd64, arm64] - uses: defenseunicorns/uds-common/.github/workflows/callable-publish.yaml@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/workflows/callable-publish.yaml@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 with: flavor: ${{ matrix.flavor }} options: --set BASE_REPO="ghcr.io/uds-packages" diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml index c481ed5..d77147c 100644 --- a/.github/workflows/scan.yaml +++ b/.github/workflows/scan.yaml @@ -18,5 +18,5 @@ jobs: packages: read # Allows reading the content of the repository's packages. id-token: write # Allows authentication to Rapidfort via OIDC. pull-requests: write # Allows writing the scan results comment to the pull request. - uses: defenseunicorns/uds-common/.github/workflows/callable-scan.yaml@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/workflows/callable-scan.yaml@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 secrets: inherit # Inherits all secrets from the parent workflow. diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c2e7a9d..7580503 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: test-flavor - uses: defenseunicorns/uds-common/.github/actions/test-flavor@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/actions/test-flavor@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 id: test-flavor outputs: upgrade-flavors: ${{ steps.test-flavor.outputs.upgrade-flavors }} @@ -43,7 +43,7 @@ jobs: matrix: type: [install, upgrade] flavor: [upstream, unicorn] - uses: defenseunicorns/uds-common/.github/workflows/callable-test.yaml@730d22c6e061153d525a6d6f932e108ae952bd46 # v1.23.0 + uses: defenseunicorns/uds-common/.github/workflows/callable-test.yaml@0cfdcaa2dbe7a539dec471edbb081c88b06fe8ec # v1.24.2 with: timeout: 30 options: --set BASE_REPO="ghcr.io/uds-packages" diff --git a/README.md b/README.md index e78b9ed..aa23e1f 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,63 @@ -# UDS Package Reference Package +# UDS Reference Package -This package is designed to serve as a reference of what a UDS Package may look like. This package is not intended to be *functional* and should only serve to show what the layout of a UDS Package can look like. +This repository serves as a practical, working example of a well-structured UDS Package. -The application itself is a simple web app built with Go. This can currently be found in the `src` directory. The function of the app is a page that can write and get queries from a bundled `postgres` database. +Inside the `.github` directory, you will find a fully runnable Go-based web application that reads and writes to a Postgres database. This repository can be referenced alongside the [UDS Documentation](https://uds.defenseunicorns.com/), as a reference guide for building, configuring, and testing own UDS packages. -### Demonstration -The following should be demonstrated within this UDS Package: -- Dependencies Pulled into a UDS Bundle -- Keycloak SSO Configuration -- Prometheus Service Monitoring -- Helm Overrides -- UDS Config Chart Templates -- Istio Virtual Service Creation -- Network Policy Creation -- Playwright UI Testing +> [!NOTE] +> **The primary purpose of this repository is to demonstrate UDS Package architecture, layout, and best practices. The application's specific features are secondary.** +## What This Demonstrates -## Pre-requisites +This repository should aim to provide functional examples of the following: -The Reference Package Package expects to be deployed on top of [UDS Core](https://github.com/defenseunicorns/uds-core) with the dependencies listed below being configured prior to deployment. +* **Bundle Integration:** Pulling dependencies into a UDS Bundle. +* **Authentication:** Keycloak SSO configuration. +* **Observability:** Prometheus service monitoring integration. +* **Configuration:** Helm overrides and UDS Config chart templates. +* **Networking & Security:** Istio Virtual Service and Network Policy creation. +* **Testing:** Playwright UI testing. -#### Postgres Database -This package requires a postgres database instance. It is suggested to pull this into the `uds-bundle` by using the `postgres-operator` uds package. -#### Monitoring -To demonstrate monitoring, the `k3d-core-demo` will need to be installed instead of `k3d-core-slim-dev`. -## Releases +## Prerequisites -The released packages can be found in [ghcr](https://github.com/uds-packages/reference-package/pkgs/container/reference-package). +This reference package is designed to be deployed on top of [UDS Core](https://github.com/defenseunicorns/uds-core). Please ensure the following dependencies are configured prior to deployment: -## UDS Tasks (for local dev and CI) +* **Postgres Database:** The Go application requires a Postgres instance. We recommend bringing this into your `bundle/uds-bundle` by using the `postgres-operator` UDS package. +* **Monitoring:** To successfully demonstrate the monitoring features, you will need to install the `k3d-core-demo` bundle rather than the `k3d-core-slim-dev` bundle. + +> [!TIP] +> `k3d-core-demo` is set as the default k3d bundle if you run `uds run default` in the root directory. + +## Quick Start +### UDS Tasks (for local dev and CI) *For local dev, this requires you install [uds-cli](https://github.com/defenseunicorns/uds-cli?tab=readme-ov-file#install) +The UDS Tasks can be found in the `tasks.yaml` > [!TIP] > To get a list of tasks to run you can use `uds run --list`! +### Deployment +#### If you already have UDS Core installed + +This will create the package, create the test-bundle, then deploy in the k3d cluster. +```bash +uds run dev +``` + +#### If you do not have UDS Core installed + +This will stand up the k3d cluster, create the package, create the test-bundle, then deploy in the k3d cluster. +```bash +uds run default +``` +#### Access the WebUI +Once the app is deployed, it can be accessed in the web browser at https://reference-package.uds.dev + +## Releases + +The released packages can be found in [ghcr](https://github.com/uds-packages/reference-package/pkgs/container/reference-package). + ## Contributing diff --git a/docs/configuration.md b/docs/configuration.md index f18c3b6..e0c55f5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,5 +1,69 @@ # Configuration -Reference Package in this package is configured through [Reference Package UDS package](#UDS_PACKAGE_REPO#) as well as a UDS configuration chart that supports the following: +The Reference Package is configured using the [application's Helm chart](https://github.com/uds-packages/reference-package/tree/main/.github/container-and-chart/helm/chart), alongside the `uds-reference-package` UDS config chart. -## Additional Configuration Info Follows +## Bundle Overrides + +Use bundle overrides in `bundle/uds-bundle.yaml` to configure the Database, SSO, and Monitoring. + +```yaml +overrides: + reference-package: + reference-package: + values: + - path: database + value: + secretName: "reference-package-postgres" + secretKey: "PASSWORD" + - path: sso + value: + enabled: true + secretName: reference-package-sso + - path: monitoring + value: + enabled: true + +``` + +## UDS Config Chart Values + +### PostgreSQL Database + +The underlying Go application requires a database connection string provided via a Kubernetes secret. + +If you are using the [uds-package-postgres-operator](https://github.com/uds-packages/postgres-operator) in your bundle, the `uds-reference-package-config` chart (located in `./chart`) will create the secret, via the below values: + +```yaml +postgres: + username: "reference" + # Note: Specifying password as anything other than "" will not use the existingSecret + password: "" + existingSecret: + name: "reference-package.reference-package.pg-cluster.credentials.postgresql.acid.zalan.do" + passwordKey: password + usernameKey: username + host: "pg-cluster.postgres.svc.cluster.local" + dbName: "reference" + # Example: "?connect_timeout=10&sslmode=disable" + connectionOptions: "?sslmode=disable" + # Set to false to use external postgres + internal: true + selector: + cluster-name: pg-cluster + namespace: postgres + port: 5432 +``` + +### Single Sign-On + +Setting `sso.enabled: true` in the UDS config chart overrides tells the package to generate an SSO secret. + +This relies on the UDS Operator's built-in secret templating. You can read more about how this works in the [UDS SSO Templating Documentation](https://uds.defenseunicorns.com/reference/configuration/single-sign-on/sso-templating/). + +### Monitoring + +Setting `monitoring.enabled: true` configures the package to expose metrics to Prometheus. More information can be found here: [Monitoring and Metrics](https://uds.defenseunicorns.com/reference/configuration/observability/monitoring-metrics/) + +## Package Custom Resources (CR) + +For further information regarding the UDS Package Custom Resource (CR), defined in the `chart/templates/uds-package.yaml`, the full specification can be found in the [UDS Package CR Documentation](https://uds.defenseunicorns.com/reference/configuration/custom-resources/exemptions-v1alpha1-cr/). diff --git a/docs/go-app.md b/docs/go-app.md new file mode 100644 index 0000000..53c1892 --- /dev/null +++ b/docs/go-app.md @@ -0,0 +1,32 @@ +# Reference Package Web Application & Helm Chart + +The underlying application for this UDS package is a lightweight Go web service. It features a simple user interface designed specifically to demonstrate reading and writing data to a Postgres database. + +While the application code itself is relatively simple, its primary purpose is to demonstrate integration with various components within the UDS ecosystem. + +## Directory Structure & Architecture + +You will find the application source code and deployment manifests in the following locations: + +* **Go Application Source & Dockerfile:** `.github/container-and-chart/docker` +* **Helm Charts:** `.github/container-and-chart/helm` + +> [!NOTE] +> In a standard development scenario, it may make more sense for the Go App and Helm Charts to live in a `src/` directory and is built at runtime. However, the application and its Helm charts are intentially decoupled into this structure. This allows the Docker container and Helm charts to be published independently of the UDS package. +> By doing this, engineers can pull and use our container and Helm chart for their own purposes. It also provides a realistic demonstration of how a UDS package pulls external artifacts via the `common/zarf.yaml` and `.zarf.yaml` files. + +## Update & Publishing Workflow + +The UDS package relies on the published artifacts in GHCR. If you make changes to the Go application or the Helm chart, you must publish the new versions to GHCR before updating the UDS package. If you update the `zarf.yaml`, image tags in `-values.yaml`, etc., before the new artifacts finish publishing, your local Zarf builds and CI pipelines will fail when trying to pull the non-existent versions. + +Follow these steps when making changes to the Go application or chart: + +1. Update the Go source code or adjust the Helm chart templates as needed. +2. If needed, bump the respective versions in `.github/container-and-chart/docker/version.txt` and `.github/container-and-chart/helm/Chart.yaml`. +3. Commit and push your changes. This will trigger the `.github/workflows/release-container-and-charts.yaml` GitHub Action, which builds and publishes the new container image and Helm chart to GHCR. This workflow is set to only run on PRs to the following: +```yaml + paths: + - ".github/container-and-chart/**" + - ".github/workflows/release-container-and-charts.yaml" +``` +4. Ensure the GitHub Action completes successfully and the new artifacts are visible in the repository's GHCR page. diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile deleted file mode 100644 index b0ae2de..0000000 --- a/src/docker/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2026 Defense Unicorns -# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -FROM golang:1.25-alpine AS builder -WORKDIR /app -# Copy dependency files first -COPY go.mod go.sum ./ -RUN go mod download - -COPY main.go index.html ./ -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o hello-world main.go - -FROM scratch -COPY --from=builder /app/hello-world /hello-world - -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -ENTRYPOINT ["/hello-world"] diff --git a/src/docker/go.mod b/src/docker/go.mod deleted file mode 100644 index 878c161..0000000 --- a/src/docker/go.mod +++ /dev/null @@ -1,23 +0,0 @@ -module reference-package - -go 1.25.6 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-oidc/v3 v3.17.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect -) diff --git a/src/docker/go.sum b/src/docker/go.sum deleted file mode 100644 index 9933b48..0000000 --- a/src/docker/go.sum +++ /dev/null @@ -1,43 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/docker/index.html b/src/docker/index.html deleted file mode 100644 index 5ea9278..0000000 --- a/src/docker/index.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - Reference Package - - - - -
-
- - -
- -

Reference Package

- -
Connecting to Database...
- -
- - - - -
- - - - - - - - - - - - - - -
KeyValueActions
Waiting for database connection...
-
- - - - - diff --git a/src/docker/main.go b/src/docker/main.go deleted file mode 100644 index 58cc224..0000000 --- a/src/docker/main.go +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright 2026 Defense Unicorns -// SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -package main - -import ( - "context" - "crypto/rand" - _ "embed" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/jackc/pgx/v5" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/oauth2" -) - -//go:embed index.html -var indexHTML string - -type KVPair struct { - Key string `json:"key"` - Value string `json:"value"` -} - -type UserInfo struct { - Username string `json:"username"` - Type string `json:"type"` -} - -// --- Global Variables --- -var ( - // SSO State - oauth2Config *oauth2.Config - oidcVerifier *oidc.IDTokenVerifier - ssoEnabled bool - - // DB State - dbConn *pgx.Conn - dbMu sync.RWMutex - - // Prometheus Metrics - dbConnectedMetric = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "app_database_connected", - Help: "Binary status of database connection (1 = connected, 0 = disconnected)", - }) - kvCountMetric = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "reference_package_kv_count", - Help: "Current number of key/value pairs stored in kv_store", - }) - httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "app_http_requests_total", - Help: "Total number of HTTP requests by path and status", - }, []string{"path", "status"}) -) - -func main() { - ctx := context.Background() - - // --- 1. Background Database Connection --- - go func() { - connStr := os.Getenv("DATABASE_URL") - if connStr == "" { - fmt.Println("DATABASE_URL not set. Running in No-DB mode.") - dbConnectedMetric.Set(0) - return - } - - for { - conn, err := pgx.Connect(context.Background(), connStr) - if err == nil { - fmt.Println("Successfully connected to Postgres!") - - // Initialize table - _, err = conn.Exec(context.Background(), "CREATE TABLE IF NOT EXISTS kv_store (key TEXT PRIMARY KEY, value TEXT)") - if err != nil { - fmt.Printf("Failed to initialize table: %v\n", err) - } - - dbMu.Lock() - dbConn = conn - dbMu.Unlock() - dbConnectedMetric.Set(1) - // Initialize KV count metric from DB - if err := updateKVCount(context.Background(), conn); err != nil { - fmt.Printf("Failed to initialize kv count metric: %v\n", err) - } - break - } - fmt.Printf("Postgres not available yet, retrying in 5s... (%v)\n", err) - dbConnectedMetric.Set(0) - time.Sleep(5 * time.Second) - } - }() - - // --- 2. SSO Setup --- - if os.Getenv("KEYCLOAK_URL") != "" { - fmt.Println("Initializing SSO...") - if err := initSSO(ctx); err != nil { - fmt.Printf("WARNING: SSO failed to initialize: %v. Running in INSECURE mode.\n", err) - ssoEnabled = false - } else { - fmt.Println("SSO Initialized successfully.") - ssoEnabled = true - } - } else { - fmt.Println("KEYCLOAK_URL not set. Running in INSECURE mode (SSO Disabled).") - ssoEnabled = false - } - - // --- 3. HTTP Routes --- - - if os.Getenv("MONITORING_ENABLED") == "true" { - http.Handle("/metrics", promhttp.Handler()) - } - - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - }) - - // Main App Page - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if !ssoEnabled { - serveApp(w) - return - } - - // Check Guest - if _, err := r.Cookie("guest_mode"); err == nil { - serveApp(w) - return - } - - // Check SSO - cookie, err := r.Cookie("auth_token") - if err != nil { - serveLogin(w) - return - } - _, err = oidcVerifier.Verify(r.Context(), cookie.Value) - if err != nil { - serveLogin(w) - return - } - - serveApp(w) - }) - - // Auth Handlers - http.HandleFunc("/login", handleLogin) - http.HandleFunc("/login-guest", handleGuestLogin) - http.HandleFunc("/callback", handleCallback) - http.HandleFunc("/logout", handleLogout) - - // User Info API - http.HandleFunc("/whoami", protect(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if _, err := r.Cookie("guest_mode"); err == nil { - json.NewEncoder(w).Encode(UserInfo{Username: "Guest", Type: "guest"}) - return - } - - cookie, err := r.Cookie("auth_token") - if err == nil { - idToken, err := oidcVerifier.Verify(r.Context(), cookie.Value) - if err == nil { - var claims struct { - Email string `json:"email"` - PreferredUsername string `json:"preferred_username"` - } - if err := idToken.Claims(&claims); err == nil { - name := claims.PreferredUsername - if name == "" { - name = claims.Email - } - json.NewEncoder(w).Encode(UserInfo{Username: name, Type: "sso"}) - return - } - } - } - - json.NewEncoder(w).Encode(UserInfo{Username: "Unknown", Type: "unknown"}) - })) - - // API: Set Value - http.HandleFunc("/set", protect(func(w http.ResponseWriter, r *http.Request) { - dbMu.RLock() - defer dbMu.RUnlock() - - if dbConn == nil { - trackRequest("/set", "503") - http.Error(w, "Database unavailable", http.StatusServiceUnavailable) - return - } - - key := r.FormValue("key") - val := r.FormValue("value") - - // --- DB LOGGING START --- - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[DB-WRITE] Executing: INSERT INTO kv_store (key, value) VALUES ('%s', '%s') ON CONFLICT UPDATE\n", key, val) - } - // --- DB LOGGING END --- - - _, err := dbConn.Exec(r.Context(), ` - INSERT INTO kv_store (key, value) VALUES ($1, $2) - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`, - key, val) - if err != nil { - trackRequest("/set", "500") - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[DB-ERROR] Write failed: %v\n", err) - } - http.Error(w, "DB Error: "+err.Error(), 500) - return - } - - trackRequest("/set", "200") - // Refresh kv count metric after successful write. Best-effort only. - if err := refreshKVCount(r.Context()); err != nil { - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[METRICS] Failed to refresh kv count: %v\n", err) - } - } - fmt.Fprint(w, "Success") - })) - - // API: Delete Value - http.HandleFunc("/delete", protect(func(w http.ResponseWriter, r *http.Request) { - dbMu.RLock() - defer dbMu.RUnlock() - - if dbConn == nil { - trackRequest("/delete", "503") - http.Error(w, "Database unavailable", http.StatusServiceUnavailable) - return - } - - key := r.FormValue("key") - if key == "" { - trackRequest("/delete", "400") - http.Error(w, "Missing key", http.StatusBadRequest) - return - } - - // --- DB LOGGING START --- - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[DB-WRITE] Executing: DELETE FROM kv_store WHERE key = '%s'\n", key) - } - // --- DB LOGGING END --- - - result, err := dbConn.Exec(r.Context(), "DELETE FROM kv_store WHERE key = $1", key) - if err != nil { - trackRequest("/delete", "500") - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[DB-ERROR] Delete failed: %v\n", err) - } - http.Error(w, "DB Error: "+err.Error(), 500) - return - } - - // If no rows affected, return 404 - if result.RowsAffected() == 0 { - trackRequest("/delete", "404") - http.Error(w, "Key not found", http.StatusNotFound) - return - } - - trackRequest("/delete", "200") - // Refresh kv count metric after successful delete. Best-effort only. - if err := refreshKVCount(r.Context()); err != nil { - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[METRICS] Failed to refresh kv count: %v\n", err) - } - } - - fmt.Fprint(w, "Success") - })) - - // API: Get All Values - http.HandleFunc("/get-all", protect(func(w http.ResponseWriter, r *http.Request) { - dbMu.RLock() - defer dbMu.RUnlock() - - if dbConn == nil { - trackRequest("/get-all", "503") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusServiceUnavailable) - json.NewEncoder(w).Encode([]KVPair{}) - return - } - - // --- DB LOGGING START --- - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Println("[DB-READ] Executing: SELECT key, value FROM kv_store ORDER BY key ASC") - } - // --- DB LOGGING END --- - - rows, err := dbConn.Query(r.Context(), "SELECT key, value FROM kv_store ORDER BY key ASC") - if err != nil { - trackRequest("/get-all", "500") - if os.Getenv("DB_LOG_LEVEL") == "debug" { - fmt.Printf("[DB-ERROR] Read failed: %v\n", err) - } - http.Error(w, "Query failed", 500) - return - } - defer rows.Close() - - var pairs []KVPair - for rows.Next() { - var p KVPair - rows.Scan(&p.Key, &p.Value) - pairs = append(pairs, p) - } - - trackRequest("/get-all", "200") - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(pairs) - })) - - fmt.Println("Server starting on :8080...") - http.ListenAndServe(":8080", nil) -} - -// --- Helper Functions --- - -func trackRequest(path, status string) { - if os.Getenv("MONITORING_ENABLED") == "true" { - httpRequestsTotal.WithLabelValues(path, status).Inc() - } -} - -// updateKVCount queries the provided connection for the number of rows in kv_store -// and sets the kvCountMetric accordingly. -func updateKVCount(ctx context.Context, conn *pgx.Conn) error { - var count int64 - row := conn.QueryRow(ctx, "SELECT COUNT(*) FROM kv_store") - if err := row.Scan(&count); err != nil { - return err - } - kvCountMetric.Set(float64(count)) - return nil -} - -// refreshKVCount is a convenience wrapper that uses the global dbConn. -func refreshKVCount(ctx context.Context) error { - dbMu.RLock() - defer dbMu.RUnlock() - if dbConn == nil { - return fmt.Errorf("db not connected") - } - return updateKVCount(ctx, dbConn) -} - -func initSSO(ctx context.Context) error { - provider, err := oidc.NewProvider(ctx, os.Getenv("KEYCLOAK_URL")) - if err != nil { - return err - } - - oauth2Config = &oauth2.Config{ - ClientID: os.Getenv("KEYCLOAK_CLIENT_ID"), - ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"), - RedirectURL: os.Getenv("APP_CALLBACK_URL"), - Endpoint: provider.Endpoint(), - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - } - oidcVerifier = provider.Verifier(&oidc.Config{ClientID: os.Getenv("KEYCLOAK_CLIENT_ID")}) - return nil -} - -func serveApp(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, indexHTML) -} - -func serveLogin(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, ` - - -
-

Authentication Required

-

Welcome to the Reference Package

- -
- - Login with SSO - - - Login As Guest - -
-
- - - `) -} - -func protect(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !ssoEnabled { - next(w, r) - return - } - if _, err := r.Cookie("guest_mode"); err == nil { - next(w, r) - return - } - cookie, err := r.Cookie("auth_token") - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - _, err = oidcVerifier.Verify(r.Context(), cookie.Value) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - next(w, r) - } -} - -func handleGuestLogin(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "guest_mode", - Value: "true", - Path: "/", - HttpOnly: true, - MaxAge: 3600, - }) - http.Redirect(w, r, "/", http.StatusFound) -} - -// Updated Logout Logic -func handleLogout(w http.ResponseWriter, r *http.Request) { - // 1. Grab the ID token before we delete the cookie - rawIDToken := "" - if cookie, err := r.Cookie("auth_token"); err == nil { - rawIDToken = cookie.Value - } - - // 2. Clear Local Cookies - http.SetCookie(w, &http.Cookie{Name: "auth_token", Value: "", Path: "/", MaxAge: -1}) - http.SetCookie(w, &http.Cookie{Name: "guest_mode", Value: "", Path: "/", MaxAge: -1}) - - // 3. If SSO is enabled and we have a token, we must call Keycloak's logout endpoint - if ssoEnabled && rawIDToken != "" { - // Construct the "Return to App" URL (Base URL of your app) - // We derive this from the callback URL environment variable - // e.g., "https://reference-package.uds.dev/callback" -> "https://reference-package.uds.dev" - redirectURI := os.Getenv("APP_CALLBACK_URL") - if u, err := url.Parse(redirectURI); err == nil { - redirectURI = fmt.Sprintf("%s://%s", u.Scheme, u.Host) - } - - // Keycloak standard logout endpoint: - // /protocol/openid-connect/logout?post_logout_redirect_uri=&id_token_hint= - logoutURL := fmt.Sprintf("%s/protocol/openid-connect/logout?post_logout_redirect_uri=%s&id_token_hint=%s", - strings.TrimSuffix(os.Getenv("KEYCLOAK_URL"), "/"), // Ensure no double slashes - url.QueryEscape(redirectURI), - rawIDToken, - ) - - http.Redirect(w, r, logoutURL, http.StatusFound) - return - } - - // If Guest or SSO disabled, just go back to home - http.Redirect(w, r, "/", http.StatusFound) -} - -func handleLogin(w http.ResponseWriter, r *http.Request) { - if !ssoEnabled { - http.Redirect(w, r, "/", http.StatusFound) - return - } - state := randomString(16) - http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound) -} - -func handleCallback(w http.ResponseWriter, r *http.Request) { - if !ssoEnabled { - http.Redirect(w, r, "/", http.StatusFound) - return - } - ctx := r.Context() - oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code")) - if err != nil { - http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) - return - } - rawIDToken, ok := oauth2Token.Extra("id_token").(string) - if !ok { - http.Error(w, "No id_token found", http.StatusInternalServerError) - return - } - _, err = oidcVerifier.Verify(ctx, rawIDToken) - if err != nil { - http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) - return - } - http.SetCookie(w, &http.Cookie{ - Name: "auth_token", - Value: rawIDToken, - HttpOnly: true, - Path: "/", - Secure: true, - MaxAge: 3600, - }) - http.Redirect(w, r, "/", http.StatusFound) -} - -func randomString(n int) string { - b := make([]byte, n) - rand.Read(b) - return base64.URLEncoding.EncodeToString(b) -} diff --git a/src/docker/version.txt b/src/docker/version.txt deleted file mode 100644 index 9ff151c..0000000 --- a/src/docker/version.txt +++ /dev/null @@ -1 +0,0 @@ -v0.1.0 \ No newline at end of file diff --git a/src/helm/chart/.helmignore b/src/helm/chart/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/src/helm/chart/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/src/helm/chart/Chart.yaml b/src/helm/chart/Chart.yaml deleted file mode 100644 index 8f9e65a..0000000 --- a/src/helm/chart/Chart.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2026 Defense Unicorns -# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -apiVersion: v2 -name: reference-package -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "0.1.0" diff --git a/src/helm/chart/templates/deployment.yaml b/src/helm/chart/templates/deployment.yaml deleted file mode 100644 index 0c3acaa..0000000 --- a/src/helm/chart/templates/deployment.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2026 Defense Unicorns -# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: reference-package -spec: - replicas: 1 - selector: - matchLabels: - app: reference-package - template: - metadata: - labels: - app: reference-package - spec: - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: reference-package - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: "{{ .Values.image.pullPolicy }}" - ports: - - containerPort: 8080 - env: - - name: DB_LOG_LEVEL - value: {{ .Values.logging.dbLevel | default "info" | quote }} - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: {{ .Values.database.secretName }} - key: {{ .Values.database.secretKey }} - - name: MONITORING_ENABLED - value: {{ .Values.monitoring.enabled | quote }} - {{- if .Values.sso.enabled }} - envFrom: - - secretRef: - name: {{ .Values.sso.secretName }} - {{- end }} diff --git a/src/helm/chart/templates/service.yaml b/src/helm/chart/templates/service.yaml deleted file mode 100644 index 2ba461f..0000000 --- a/src/helm/chart/templates/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2026 Defense Unicorns -# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -apiVersion: v1 -kind: Service -metadata: - name: reference-package - labels: - app: reference-package -spec: - selector: - app: reference-package - ports: - - name: http - port: 8080 - targetPort: 8080 diff --git a/src/helm/chart/values.yaml b/src/helm/chart/values.yaml deleted file mode 100644 index c9e632c..0000000 --- a/src/helm/chart/values.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2026 Defense Unicorns -# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial - -image: - repository: reference-package - tag: latest - pullPolicy: IfNotPresent - -service: - type: ClusterIP - port: 80 - targetPort: 8080 - -logging: - dbLevel: "info" # error, warn, info, debug diff --git a/tasks.yaml b/tasks.yaml index 7d66e6d..7e1fe5c 100644 --- a/tasks.yaml +++ b/tasks.yaml @@ -3,15 +3,14 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/defenseunicorns/uds-cli/refs/heads/main/tasks.schema.json includes: - test: ./tasks/test.yaml - - create: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/create.yaml - - lint: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/lint.yaml - - pull: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/pull.yaml - - deploy: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/deploy.yaml - - setup: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/setup.yaml - - actions: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/actions.yaml - - badge: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/badge.yaml - - upgrade: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/upgrade.yaml - - publish: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.23.0/tasks/publish.yaml + - create: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/create.yaml + - lint: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/lint.yaml + - pull: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/pull.yaml + - deploy: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/deploy.yaml + - setup: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/setup.yaml + - actions: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/actions.yaml + - upgrade: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/upgrade.yaml + - publish: https://raw.githubusercontent.com/defenseunicorns/uds-common/v1.24.2/tasks/publish.yaml tasks: - name: default @@ -58,6 +57,7 @@ tasks: - task: upgrade:create-latest-tag-bundle - task: setup:k3d-test-cluster - task: deploy:test-bundle + - task: create-dev-package - task: create-deploy-test-bundle - name: publish-package diff --git a/values/unicorn-values.yaml b/values/unicorn-values.yaml index 5460759..2b15afb 100644 --- a/values/unicorn-values.yaml +++ b/values/unicorn-values.yaml @@ -4,4 +4,4 @@ image: repository: ghcr.io/uds-packages/reference-package/container/reference-package tag: v0.1.0 - pullPolicy: Always + pullPolicy: IfNotPresent diff --git a/values/upstream-values.yaml b/values/upstream-values.yaml index 5460759..2b15afb 100644 --- a/values/upstream-values.yaml +++ b/values/upstream-values.yaml @@ -4,4 +4,4 @@ image: repository: ghcr.io/uds-packages/reference-package/container/reference-package tag: v0.1.0 - pullPolicy: Always + pullPolicy: IfNotPresent