Compare commits

...

189 Commits

Author SHA1 Message Date
Jeff McCune
7956475363 (#178) Add BuildPlan deployFiles field
Previously, each BuildPlan has no clear way to produce an ArgoCD
Application resource.  This patch provides a general solution where each
BuildPlan can provide arbitrary files as a map[string]string where the
key is the file path relative to the gitops repository `deploy/` folder.
2024-06-03 10:00:35 -07:00
Jeff McCune
004ed56591 (#178) Add ArgoCD repository credentials
Previously ArgoCD has no ssh credentials to connect to GitHub.  This
patch adds an ssh ed25519 key as a secret in the management cluster.
The secret is synced to the workload clusters using an ExternalSecret
with the proper label for ArgoCD to find and load it for use with any
application that references the Git URL.
2024-06-02 15:58:35 -07:00
Jeff McCune
d497df3c27 (#178) Add ArgoCD RBAC Policy
Previously a logged in user could not modify anything in ArgoCD.  With
this patch users who have been granted the prod-cluster-admin role in
ZITADEL are granted the admin role in ArgoCD.
2024-06-02 15:07:27 -07:00
Jeff McCune
3a8d46234f (#178) Add ArgoCD
Previously ArgoCD was present in the platform configuration, but not
functional.  This patch brings ArgoCD fully up, integrated with the
service mesh, auth proxy, and SSO at
https://argocd.admin.clustername.example.com/

The upstream [helm chart][1] is used instead of the kustomize install
method.  We had existing prior art integrating the v6 helm chart with
the holos platform identity provider, so we continue with the helm
chart.

CRDs are still managed with the kustomize version.  The CRDs need to be
kept in sync.  It's possible to generate the kustomization.yaml file
from the same version value as is used by the helm chart, but we don't
for the time being.

[1]: https://github.com/argoproj/argo-helm/tree/argo-cd-7.1.1/charts/argo-cd
2024-06-02 14:35:36 -07:00
Jeff McCune
4d24dc5149 (#178) Add authpolicy component for RequestAuthentication
Previously, no RequestAuthentication or AuthorizationPolicy resources
govern the default Gateway.  This patch adds the resources and
configures the service mesh with the authproxy as an ExtAuthZ provider
for CUSTOM AuthorizationPolicy rules.

This patch also fixes a bug in the zitadel-server component where
resources from the upstream helm chart did not specify a namespace.
Kustomize is used as a post processor to force all resources into the
zitadel namespace.

Add multiple HTTPRoutes to validate http2 connection reuse

This patch adds multiple HTTPRoute resources which match
*.admin.example.com  The purpose is to validate http2 connections are
reused properly with Chrome.

With this patch no 404 no route errors are encountered when navigating
between the various httpbin{1,2,3,4} urls.

Add note backupRestore will trigger a restore

The process of configuring ZITADEL to provision from a datasource will
cause an in-place restore from S3.  This isn't a major issue, but users
should be aware data added since the most recent backup will be lost.
2024-06-02 09:41:57 -07:00
Jeff McCune
8eb7fbf7dc (#178) Move httpbin HTTPRoute resources to namespace istio-gateways
Previously, HTTPRoute resources were in the same namespace as the
backend service, httpbin in this case.  This doesn't follow the default
behavior of a Gateway listener only allowing attachment from HTTPRoute
resources in the same namespace as the Gateway.

This also complicates intercepting the authproxy path prefix and sending
it to the authproxy.  We'd need to add a ReferenceGrant in the authproxy
namespace, which seems backwards and dangerous because it would grant
the application developer the ability to route requests to all Services
in the istio-gateways namespace.

This patch enables Cluster Operators to manage the HTTPRoute resources
and direct the auth proxy path prefix of `/holos/authproxy` to the auth
proxy Service in the same namespace.

ReferenceGrant resources are used to enable the HTTPRoute backend
references.

When an application developer needs to manage their own HTTPRoute, as is
the case for ZITADEL, a label selector may be used and will override
less specific HTTPRoute hostsnames in the istio-gateways namespace.
2024-06-01 21:18:47 -07:00
Jeff McCune
ffeeb7c553 (#178) Add authproxy Deployment
With redis.  The auth proxy authenticates correctly against zitadel
running in the same cluster.  Validated by visiting
https://httpbin.admin.clustername.example.com/holos/authproxy

Visiting
https://httpbin.admin.clustername.example.com/holos/authproxy/auth
returns the id token in the response header, visible in the Chrome
network inspector.  The ID token works as expected from multiple orgs
with project grants in ZITADEL from the Holos org to the OIS org.

This patch doesn't fully implement the auth proxy feature.
AuthorizationPolicy and RequestAuthentication resources need to be
added.

Before we do so, we need to move the HTTPRoute resources into the
gateway namespace so all of the security policies are in one place and
to simplify the process of routing requests to two backends, the
authproxy and the backend server.
2024-06-01 20:12:35 -07:00
Jeff McCune
c3c174155c (#178) Add httpbin{1,2,3,4} HTTPRoutes to validate http2 connection reuse
This patch adds multiple HTTPRoute resources which match
*.admin.example.com  The purpose is to validate http2 connections are
reused properly with Chrome.

With this patch no 404 no route errors are encountered when navigating
between the various httpbin{1,2,3,4} urls.
2024-06-01 12:44:33 -07:00
Jeff McCune
2c2d2a9fd9 (#178) Add Namespaces documentation
Describe how to manage a new namespace to build a component in.
2024-06-01 09:43:32 -07:00
Jeff McCune
d692e2a6d5 (#178) Split subdomain certs into two certs
Problem:
Istio 1.22 with Gateway API and HTTPRoute is mis-routing HTTP2 requests
when the tls certificate has two dns names, for example
login.example.com and *.login.example.com.

When the user visits login.example.com and then tries to visit
other.login.example.com with Chrome, the connection is re-used and istio
returns a 404 route not found error even though there is a valid and
accepted HTTPRoute for *.login.example.com

This patch attempts to fix the problem by ensuring certificate dns names
map exactly to Gateway listeners.  When a wildcard cert is used, the
corresponding Gateway listener host field exactly matches the wildcard
cert dns name so Istio and envoy should not get confused.
2024-06-01 09:30:47 -07:00
Jeff McCune
e4cebddd0c (#178) Make aws2 the primary cluster 2024-05-31 14:01:11 -07:00
Jeff McCune
0e48537d65 (#178) Add zitadel-server component
This patch adds the ZITADEL server component, which deploys zitadel from
a helm chart.  Kustomize is used heavily to patch the output of helm to
make the configuration fit nicely with the holos platform.

With this patch the two Jobs that initialize the database and setup
ZITADEL run successfully.  The ZITADEL deployment starts successfully.

ZITADEL is accessible at https://login.example.com/ with the default
admin username of `zitadel-admin@zitadel.login.example.com` and password
`Password1!`.

Use grant.holos.run/subdomain.admin: "true" for HTTPRoute

This patch clarifies the label that grants httproute attachment for a
subdomain Gateway listener to a namespace.

Fix istio-base holos component name

Was named `base` which is the chart name, not the holos component name.
2024-05-31 13:47:03 -07:00
Jeff McCune
a461a96b9c (#178) Add ZITADEL crunchy pgo PostgresCluster
This patch adds the postgres clusters and a few console form controls to
configure how backups are taken and if the postgres cluster is
initialized from an existing backup or not.

The pgo-s3-creds file is manually created at this time.  It looks like:

    ❯ holos get secret -n zitadel pgo-s3-creds --print-key s3.conf
    [global]
    repo2-cipher-pass=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    repo2-s3-key=KKKKKKKKKKKKKKKKKKKK
    repo2-s3-key-secret=/SSSSSSS/SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
    repo3-cipher-pass=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    repo3-s3-key=KKKKKKKKKKKKKKKKKKKK
    repo3-s3-key-secret=/SSSSSSS/SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS

The s3 key and secret are credentials to read / write to the bucket.
The cipher pass is a random string for client side encryption.  Generate
it with `tr -dc A-Za-z0-9 </dev/urandom | head -c 64`
2024-05-30 11:33:00 -07:00
Jeff McCune
9524c4f7c3 (#178) Add crunchy postgres operator
Needed for ZITADEL and Holos Server.  Intended for ephemeral dev
environments, but may also try it in staging while we wait for RDS.
2024-05-29 12:03:05 -07:00
Jeff McCune
64b04d9cfd (#178) Add Gateway listener for login.example.com
This patch is foundational work for the ZITADEL login service.

This patch adds a tls certificate with names *.login.example.com and
login.example.com, a pair of listeners attached to the certificate in
the `default` Gateway, and the ExternalSecret to sync the secret from
the management cluster.

The zitadel namespace is managed and has the label
holos.run/login.grant: "true" to grant HTTPRoute attachment from the
zitadel namespace to the default Gateway in the istio-gateways
namespace.
2024-05-29 09:27:08 -07:00
Jeff McCune
b419ad8caf (#178) Add HTTPRoute for httpbin.admin.aws1.example.com
With this change, https://httpbin.admin.aws1.example.com works as
expected.

PROXY protocol is configured on the AWS load balancer and the istio
gateway.  The istio gateway logs have the correct client source ip
address and x-forwarded-for headers.

Namespaces must have the holos.run/admin.grant: "true" label in order to
attach an HTTPRoute to the admin section of the default Gateway.

The TLS certificate is working as expected and hopefully does not suffer
from the NR route not found issued encountered with the Istio Gateway
API.
2024-05-28 21:10:18 -07:00
Jeff McCune
8036c17916 (#178) Add istiod and gateway components
This patch gets the istio-ingressgateway up and running in AWS with
minimal configuration.  No authentication or authorization policies have
been migrated from previous iterations of the platform.  These will be
handled in subsequent iterations.

Connectivity to a backend service like httpbin has not yet been tested.
This will happen in a follow up as well using /httpbin path prefixes on
existing services like argocd to conserve certificate resources.
2024-05-28 14:37:25 -07:00
Jeff McCune
220d498be0 (#178) Define a #IngressCertificate
This is the standard way to issue public facing certificates.  Be aware
of the 50 cert limit per week from LetsEncrypt.  We map names to certs
1:1 to avoid http2 connection reuse issues with istio.
2024-05-28 13:15:14 -07:00
Jeff McCune
0f5b6a2d6e (#178) Add istio 1.22.0 base component 2024-05-28 13:08:34 -07:00
Jeff McCune
36369d75c7 (#178) Add argocd.admin.aws1.holos.run cert
Manage certificates on a project basis similar to how namespaces
associated with each project are managed.

Manage the Certificate resources on the management cluster in the
istio-ingress namespace so the tls certs can be synced to the workload
clusters.
2024-05-28 11:50:31 -07:00
Jeff McCune
059b8283fd (#178) Add cert-letsencrypt component for holos management cluster
The secret needs to be manually provisioned for this to work since the
management cluster does not sync secrets from any other external
cluster.
2024-05-26 09:56:46 -07:00
Jeff McCune
386eb2452a (#178) Add cert-manager to the holos platform
This patch adds cert-manager on all clusters.  On the management cluster
cert manager is scheduled on spot instances to reduce cost.
2024-05-26 09:29:15 -07:00
Jeff McCune
38e9a97fd2 (#178) Add secretstores holos platform component
The secretstores component is critical and provides the mechanism to
securely fetch Secret resources from the Management Cluster.
The holos server and configuration code stored in version control
contains only ExternalSecret references, no actual secrets.

This component adds a `default` `SecretStore` to each management
namespace which uses the `eso-reader` service account token to
authenticate to the management cluster.  This service account is limited
to reading secrets within the namespace it resides in.

For example:

```yaml
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: default
  namespace: external-secrets
spec:
  provider:
    kubernetes:
      auth:
        token:
          bearerToken:
            key: token
            name: eso-reader
      remoteNamespace: external-secrets
      server:
        caBundle: Long Base64 encoded string
        url: https://34.121.54.174
```
2024-05-25 15:02:06 -07:00
Jeff McCune
ecca40e9d5 (#178) Add holos platform eso-creds-manager
This patch adds the `eso-creds-manager` component which needs to be
applied to the management cluster prior to the `eso-creds-refreher`
component being applied to workload clusters.

The manager component configures rbac to allow the creds-refresher job
to complete.

This patch also adjusts the behavior to only create secrets for the
eso-reader account by default.

Namespaces with the label `holos.run/eso.writer=true` will also have an
eso-writer secret provisioned in their namespace, allowing secrets to be
written back to the management cluster.  This is intended for the
PushSecret resource.
2024-05-24 15:09:59 -07:00
Jeff McCune
9d08e27e31 (#178) Add cue.mod/gen/k8s.io/api/batch/v1 2024-05-23 16:33:58 -07:00
Jeff McCune
969bf5e867 (#178) Import k8s rbac api
cue get go k8s.io/api/rbac/v1beta1
cue get go k8s.io/api/rbac/v1
2024-05-23 16:26:24 -07:00
Jeff McCune
3b5f28f4df (#178) Fix holos generate writing executable files
Adhere to the umask to allow group writable or world writable, but do
not set the execute bit.
2024-05-23 11:37:04 -07:00
Jeff McCune
df5619f988 (#178) Add ArgoCD schematic and component to holos
Add the ArgoCD component which is a good example of how to wrap a plain
kustomize kustomization.yaml file with Holos.
2024-05-23 11:18:29 -07:00
Jeff McCune
a6d8383176 (#178) Do not write flux kustomization if empty
If the holos component returns no data for the flux kustomization, don't
bother writing an useless empty file.
2024-05-23 10:56:08 -07:00
Jeff McCune
dbc7e374cd (#178) Update buf 2024-05-23 09:37:46 -07:00
Jeff McCune
d81729857b (#178) v0.81.2 for holos
Use v0.81.2 to build out the holos platform.  Once we have the
components structured fairly well we can circle back around and copy the
components to schematics.  There's a bit of friction regenerating the
platform from schematic each time.
2024-05-23 09:14:27 -07:00
Jeff McCune
d3d8a7b73c (#178) Shape _Namespaces to corev1.#Namespace
Eliminate the need for a for loop by having _Namespaces be a struct of
name to k8s.io/api/core/v1.#Namespace
2024-05-23 09:12:08 -07:00
Jeff McCune
d9e6776b95 (#178) npm upgrade 2024-05-23 06:41:10 -07:00
Jeff McCune
bde98faffa (#178) Use private fields to store data
Using CUE definitions like #Platform to hold data is confusing.  Clarify
the use of fields, definitions like #Platform define the shape (schema)
of the data while private fields like _Platform represent and hold the
data.
2024-05-23 06:38:52 -07:00
Jeff McCune
c2847554e0 (#178) Add namespaces to holos platform 2024-05-22 17:04:47 -07:00
Jeff McCune
9411a65dd8 (#178) Add namespaces component schematic
The first thing most platforms need to do is come up with a strategy for
managing namespaces across multiple clusters.

This patch defines #Namespaces in the holos platform and adds a
namespaces component which loops over all values in the #Namespaces
struct and manages a kubernetes Namespace object.

The platform resource itself loops over all clusters in the platform to
manage all namespaces across all clusters.

From a blank slate:

```
❯ holos generate platform holos
4:26PM INF platform.go:79 wrote platform.metadata.json version=0.82.0 platform_id=018fa1cf-a609-7463-aa6e-fa53bfded1dc path=/home/jeff/workspace/holos-run/holos-infra/saas/platform.metadata.json
4:26PM INF platform.go:91 generated platform holos version=0.82.0 platform_id=018fa1cf-a609-7463-aa6e-fa53bfded1dc path=/home/jeff/workspace/holos-run/holos-infra/saas

❯ holos pull platform config .
4:26PM INF pull.go:64 pulled platform model version=0.82.0 server=https://jeff.app.dev.k2.holos.run:443 platform_id=018fa1cf-a609-7463-aa6e-fa53bfded1dc
4:26PM INF pull.go:75 saved platform config version=0.82.0 server=https://jeff.app.dev.k2.holos.run:443 platform_id=018fa1cf-a609-7463-aa6e-fa53bfded1dc path=platform.config.json

❯ (cd components && holos generate component cue namespaces)
4:26PM INF component.go:147 generated component version=0.82.0 name=namespaces path=/home/jeff/workspace/holos-run/holos-infra/saas/components/namespaces

❯ holos render platform ./platform/
4:26PM INF platform.go:29 ok render component version=0.82.0 path=components/namespaces cluster=management num=1 total=2 duration=464.055541ms
4:26PM INF platform.go:29 ok render component version=0.82.0 path=components/namespaces cluster=aws1 num=2 total=2 duration=467.978499ms
```

The result:

```sh
cat deploy/clusters/management/components/namespaces/namespaces.gen.yaml
```

```yaml
---
metadata:
  name: holos
  labels:
    kubernetes.io/metadata.name: holos
kind: Namespace
apiVersion: v1
```
2024-05-22 16:32:59 -07:00
Jeff McCune
9c1165e77e (#178) Save platform.config.json with multiple lines 2024-05-22 14:10:28 -07:00
Jeff McCune
a02c7a4015 (#178) Fix the PlatformService CreatePlatform rpc
Without this patch the
holos.platform.v1alpha1.PlatformService.CreatePlatform doesn't work as
expected.  The Platform message is used which incorrectly requires a
client supplied id which is ignored by the server.

This patch allows the creation of a new platform by reusing the update
operation as a mutation that applies to both create and update.  Only
modifiable fields are part of the PlatformMutation message.
2024-05-22 12:39:24 -07:00
Jeff McCune
bdcde88e6f (#175) Add git describe to --version output
Much easier to track changes between releases.
2024-05-21 13:21:27 -07:00
Jeff McCune
a32b100192 (#175) Output at the end
Flip the let definitions to before their use to avoid confusing /
distracting users who are just getting started.

User feedback from Nate.
2024-05-21 13:03:22 -07:00
Jeff McCune
670d716403 (#175) Add podinfo oci example
This patch adds to more example helm chart based components.  podinfo
installs as a normal https repository based helm chart.  podinfo-oci
uses an oci image to manage the helm chart.

The way holos handls OCI images is subtle, so it's good to include an
example right out of the chute.  Github actions uses OCI images for
example.
2024-05-21 12:36:45 -07:00
Jeff McCune
bba3895f35 (#175) Add holos generate component helm command
This patch adds a schematic to generate a holos component that wraps a
helm chart.  The cert-manager chart is the current example.

Usage:

```bash
set -euo pipefail

rm -rf ~/holos/dev/bare
mkdir ~/holos/dev/bare
cd ~/holos/dev/bare

holos generate platform bare
holos pull platform config .
holos render platform ./platform/
(cd components && holos generate component helm cert-manager)
```

The chart builds:

```bash
holos build ./components/cert-manager | yq .
```

And renders:

```bash
holos render component ./components/cert-manager --cluster-name k2
find deploy -type f
```

```txt
9:41PM INF render.go:83 rendered cert-manager version=0.81.1 cluster=k2 status=ok action=rendered name=cert-manager
deploy/clusters/k2/holos/components/cert-manager-kustomization.gen.yaml
deploy/clusters/k2/components/cert-manager/cert-manager.gen.yaml
```
2024-05-21 11:05:53 -07:00
Jeff McCune
9e60ddbe85 (#175) Add holos generate component cue command
This patch adds a command to generate CUE based holos components from
examples embedded in the executable.  The examples are passed through
the go template rendering engine with values pulled from flags.

Each directory in the embedded filesystem becomes a unique command for
nice tab completion.  The `--name` flag defaults to "example" and is the
resulting component name.

A follow up patch with more flags will set the stage for a Helm
component schematic.

```
holos generate component cue minimal
```

```txt
3:07PM INF component.go:91 generated component version=0.80.2 name=example path=/home/jeff/holos/dev/bare/components/example
```
2024-05-20 15:10:54 -07:00
Jeff McCune
44334fca52 (#175) Fix lint 2024-05-20 12:39:43 -07:00
Jeff McCune
2e2ed398c6 (#175) Fix tests 2024-05-20 11:32:29 -07:00
Jeff McCune
34f2a52cb7 (#175) Add holos render platform command
Split holos render into component and platform.

This patch splits the previous `holos render` command into subcommands.
`holos render component ./path/to/component/` behaves as the previous
`holos render` command and renders an individual component.

The new `holos render platform ./path/to/platform/` subcommand makes
space to render the entire platform using the platform model pulled from
the PlatformService.

Starting with an empty directory:

```sh
holos register user
holos generate platform bare
holos pull platform config .
holos render platform ./platform/
```

```txt
10:01AM INF platform.go:29 ok render component version=0.80.2 path=components/configmap cluster=k1 num=1 total=1 duration=448.133038ms
```

The bare platform has a single component which refers to the platform
model pulled from the PlatformService:

```sh
cat deploy/clusters/mycluster/components/platform-configmap/platform-configmap.gen.yaml
```

```yaml
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: platform
  namespace: default
data:
  platform: |
    spec:
      model:
        cloud:
          providers:
            - cloudflare
        cloudflare:
          email: platform@openinfrastructure.co
        org:
          displayName: Open Infrastructure Services
          name: ois
```
2024-05-20 10:41:24 -07:00
Jeff McCune
d3888a884f (#175) go mod tidy 2024-05-20 06:32:53 -07:00
Jeff McCune
3845871368 (#175) holos pull platform config
This patch adds a subcommand to pull the data necessary to construct a
PlatformConfig DTO.  The PlatformConfig message contains all of the
fields and values necessary to build a platform and the platform
components.  This is an alternative to holos passing multiple tags to
CUE.  The PlatformConfig is marshalled and passed once.

The platform config is also stored in the local filesystem in the root
directory of the platform.  This enables repeated local building and
rendering without making an rpc call.

The build / render pipeline is expected to cache the PlatformConfig once
at the start of the pipeline using the pull subcommand.
2024-05-19 08:27:21 -07:00
Jeff McCune
a3b2d19adb (#175) Render the platform with the model
The `holos render platform` command is unimplemented.  This patch
partially implements platform rendering by fetching the platform model
from the PlatformService and providing it to CUE using a tag.

CUE returns a `kind: Platform` resource to `holos` which will eventually
process a Buildlan for each platform component listed in the Platform
spec.

For now, however, it's sufficient to have the current platform model
available to CUE.
2024-05-18 11:40:30 -07:00
Jeff McCune
e4e7cd8c47 (#175) Make holos render --cluster-name flag optional
Problem:
Rendering the whole platform doesn't need a cluster name.

Solution:
Make the flag optional, do not set the cue tag if it's empty.

Result:
Holos renders the platform resource and proceeds to the point where we
need to implement the iteration over platform components, passing the
platform model to each one and rendering the component.
2024-05-17 15:48:36 -07:00
Jeff McCune
fb22e5521b (#175) Define the Platform resource in CUE
We need to output a kind: Platform resource from cue so holos can
iterate over each build plan.  The platform resource itself should also
contain a copy of the platform model obtained from the PlatformService
so holos can easily pass the model to each BuildPlan it needs to execute
to render the full platform.

This patch lays the groundwork for the Platform resource.  A future
patch will have the holos cli obtain the platform model and inject it as
a JSON encoded string to CUE.  CUE will return the Platform resource
which is a list of references to build plans.  Holos will then iterate
over each build plan, pass the model back in, and execute the build
plan.

To illustrate where we're headed, the `cue export` step will move into
`holos` with a future patch.

```
❯ holos register user
3:34PM INF register.go:77 user version=0.80.0 email=jeff@ois.run server=https://app.dev.k2.holos.run:443 user_id=018f8839-3d74-7e39-afe9-181ad2fc8abe org_id=018f8839-3d74-7e3a-918c-b36494da0115
❯ holos generate platform bare
3:34PM INF generate.go:79 wrote platform.metadata.json version=0.80.0 platform_id=018f8839-3d74-7e3b-8cb8-77a2c124d173 path=/home/jeff/holos/dev/bare/platform.metadata.json
3:34PM INF generate.go:91 generated platform bare version=0.80.0 platform_id=018f8839-3d74-7e3b-8cb8-77a2c124d173 path=/home/jeff/holos/dev/bare
❯ holos push platform form .
3:34PM INF push.go:70 pushed: https://app.dev.k2.holos.run:443/ui/platform/018f8839-3d74-7e3b-8cb8-77a2c124d173 version=0.80.0
❯ cue export ./platform/
{
    "metadata": {
        "name": "bare",
        "labels": {},
        "annotations": {}
    },
    "spec": {
        "model": {}
    },
    "kind": "Platform",
    "apiVersion": "holos.run/v1alpha1"
}
```
2024-05-17 15:34:56 -07:00
Jeff McCune
d2ae766ae3 Merge pull request #176 from holos-run/dependabot/go_modules/github.com/docker/docker-26.0.2incompatible
Bump github.com/docker/docker from 26.0.0+incompatible to 26.0.2+incompatible
2024-05-17 11:53:44 -07:00
dependabot[bot]
c0db949729 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.0.0+incompatible to 26.0.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.0.0...v26.0.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-17 18:52:51 +00:00
Jeff McCune
d2d4337ffd (#175) Improve url output
❯ holos push platform form .
11:49AM INF push.go:70 pushed: https://app.dev.k2.holos.run:443/ui/platform/018f87d1-7ca2-7e37-97ed-a06bcee9b442 version=0.79.0
2024-05-17 11:49:04 -07:00
Jeff McCune
b0ca04635e (#175) Update the client context when switching servers
When the holos server URL switches, we also need to update the client
context to get the correct org id.

Also improve quality of life by printing the url to the form when the
platform form is pushed to the server.

❯ holos push platform form .
11:41AM INF push.go:71 updated platform form version=0.79.0 server=https://app.dev.k2.holos.run:443 platform_id=018f87d1-7ca2-7e37-97ed-a06bcee9b442
11:41AM INF push.go:72 https://app.dev.k2.holos.run:443/ui/platform/018f87d1-7ca2-7e37-97ed-a06bcee9b442 version=0.79.0
2024-05-17 11:43:52 -07:00
Jeff McCune
198c66e6cd (#175) Fix tests
Not sure why this started failing, but it wasn't necessary.
2024-05-17 10:22:35 -07:00
Jeff McCune
24346b9a38 (#172) Deploy v0.79.0 to dev 2024-05-17 10:15:05 -07:00
Jeff McCune
0639562f1c (#175) go mod tidy 2024-05-17 10:09:40 -07:00
Jeff McCune
c1fa9cc531 (#175) Fix lint 2024-05-17 10:08:06 -07:00
Jeff McCune
18653534ad (#175) Add holos push platform form command
This sub-command renders the web app form from CUE code and updates the
form using the `holos.platform.v1alpha1.PlatformService/UpdatePlatform`
rpc method.

Example use case, starting fresh:

```
rm -rf ~/holos
mkdir ~/holos
cd ~/holos
```

Step 1: Login

```sh
holos login
```

```txt
9:53AM INF login.go:40 logged in as jeff@ois.run version=0.79.0 name="Jeff McCune" exp="2024-05-17 21:16:07 -0700 PDT" email=jeff@ois.run
```

Step 2: Register to create server side resources.

```sh
holos register user
```

```
9:52AM INF register.go:68 user version=0.79.0 email=jeff@ois.run user_id=018f826d-85a8-751d-81ee-64d0f2775b3f org_id=018f826d-85a8-751e-98dd-a6cddd9dd8f0
```

Step 3: Generate the bare platform in the local filesystem.

```sh
holos generate platform bare
```

```txt
9:52AM INF generate.go:79 wrote platform.metadata.json version=0.79.0 platform_id=018f826d-85a8-751f-96d0-0d2bf70df909 path=/home/jeff/holos/platform.metadata.json
9:52AM INF generate.go:91 generated platform bare version=0.79.0 platform_id=018f826d-85a8-751f-96d0-0d2bf70df909 path=/home/jeff/holos
```

Step 4: Push the platform form to the `holos server` web app.

```sh
holos push platform form .
```

```txt
9:52AM INF client.go:67 updated platform version=0.79.0 platform_id=018f826d-85a8-751f-96d0-0d2bf70df909 duration=73.62995ms
```

At this point the platform form is published and functions as expected
when visiting the platform web interface.
2024-05-17 09:51:36 -07:00
Jeff McCune
2b89c33067 (#175) Add holos orgid command
Makes it easier to work with grpcurl:

    grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"org_id":"'$(holos orgid)'"}' ${HOLOS_SERVER##*/} holos.platform.v1alpha1.PlatformService.ListPlatforms
2024-05-16 21:11:24 -07:00
Jeff McCune
aee26d9375 (#175) Set header User-Agent: holos/0.70.0 (go1.22.2)
Previously: User-Agent: connect-go/1.16.0 (go1.22.2)
2024-05-16 20:49:06 -07:00
Jeff McCune
7b04d492ab (#175) Set http.Server ReadHeaderTimeout
Upstream connectrpc recommends it.  Refer to
https://connectrpc.com/docs/faq#stream-error
2024-05-16 20:28:31 -07:00
Jeff McCune
8abd03e165 (#175) Log x-request-id and x-b3-trace headers
This patch logs the x-request-id header which makes it straight forward
to correlate the logs with the service mesh logs.

For example, select the request id from the gateway logs by copying the
log from the holos server logs.

```sh
kubectl -n istio-ingress logs -l app=istio-ingressgateway -f \
  | grep --line-buffered '^{' \
  | jq 'select(.request_id=="'d0867115-5795-4096-942e-5ac188cdf618'")'
```

```json
{
  "upstream_local_address": "10.244.1.51:44248",
  "x_forwarded_for": "192.168.2.21",
  "authority": "jeff.app.dev.k2.holos.run:443",
  "upstream_transport_failure_reason": null,
  "connection_termination_details": null,
  "response_code": 200,
  "duration": 6,
  "response_flags": "-",
  "upstream_service_time": "5",
  "upstream_cluster": "outbound|3000||holos.jeff-holos.svc.cluster.local",
  "upstream_host": "10.244.1.249:3000",
  "user_agent": "connect-go/1.16.0 (go1.22.2)",
  "requested_server_name": "jeff.app.dev.k2.holos.run",
  "request_id": "d0867115-5795-4096-942e-5ac188cdf618",
  "start_time": "2024-05-17T03:16:37.900Z",
  "method": "POST",
  "protocol": "HTTP/2",
  "downstream_local_address": "65.102.23.41:443",
  "path": "/holos.user.v1alpha1.UserService/GetUser",
  "bytes_sent": 159,
  "downstream_remote_address": "192.168.2.21:59564",
  "response_code_details": "via_upstream",
  "bytes_received": 0,
  "route_name": "holos-api"
}
```
2024-05-16 20:14:34 -07:00
Jeff McCune
2df843bc98 (#175) Link the generated platform to holos server
When the user generates a platform, we need to know the platform ID it's
linked to in the holos server.  If there is no platform with the same
name, the `holos generate platform` command should error out.

This is necessary because the first thing we want to show is pushing an
updated form to `holos server`.  To update the web ui the CLI needs to
know the platform ID to update.

This patch modifies the generate command to obtain a list of platforms
for the org and verify the generated name matches one of the platforms
  that already exists.

A future patch could have the `generate platform` command call the
`holos.platform.v1alpha1.PlatformService.CreatePlatform` method if the
platform isn't found.

Results:

```sh
holos generate platform bare
```

```txt
4:15PM INF generate.go:77 wrote platform.metadata.json version=0.77.1 platform_id=018f826d-85a8-751f-96d0-0d2bf70df909 path=/home/jeff/holos/platform.metadata.json
4:15PM INF generate.go:89 generated platform bare version=0.77.1 platform_id=018f826d-85a8-751f-96d0-0d2bf70df909 path=/home/jeff/holos
```

```sh
cat platform.metadata.json
```

```json
{
  "id": "018f826d-85a8-751f-96d0-0d2bf70df909",
  "name": "bare",
  "display_name": "Bare Platform"
}
```
2024-05-16 16:18:38 -07:00
Jeff McCune
be4d2c29a5 (#175) Log info message when generating a platform
holos generate platform bare
    2:11PM INF generate.go:55 generated platform bare version=0.77.1 path=/home/jeff/holos
2024-05-16 14:26:51 -07:00
Jeff McCune
8ce88bf491 (#175) Fix goreleaser
Buf was being automatically updated in the pipeline.
2024-05-16 14:00:37 -07:00
Jeff McCune
b05571a595 (#175) Go tidy and update package.json
For goreleaser
2024-05-16 13:41:47 -07:00
Jeff McCune
4edfc71d68 (#175) Log the grpc procedure at info level
This patch logs the service and rpc method of every request at Info
level.  The error code and message is also logged.  This gives a good
indication of what rpc methods are being called and by whom.
2024-05-16 11:43:20 -07:00
Jeff McCune
3049694a0a (#175) holos register user
This patch adds a `holos register user` command.  Given an authenticated
id token and no other record of the user in the database, the cli tool
use the API to:

 1. User is registered in `holos server`
 2. User is linked to one Holos Organization.
 3. Holos Organization has the `bare` platform.
 4. Holos Organization has the `reference` platform.
 5. Ensure `~/.holos/client-context.json` contains the user id and an
    org id.

The `holos.ClientContext` struct is intended as a light weight way to
save and load the current organization id to the file system for further
API calls.

The assumption is most users will have only one single org.  We can add
a more complicated config context system like kubectl uses if and when
we need it.
2024-05-16 10:51:40 -07:00
Jeff McCune
5860c5747b (#87) generate sub-command with embedded platform
This patch adds a generate subcommand that copies a platform embedded
into the executable to the local filesystem.  The purpose is to
accelerate initial setup with canned example platforms.

Two platforms are intended to start, one bare and one reference
platform.  The number of platforms embedded into holos should be kept
small (2-3) to limit our support burden.
2024-05-14 15:03:21 -07:00
Jeff McCune
d3c2d55706 (#172) Deploy v0.76.0 to dev 2024-05-14 13:28:19 -07:00
Jeff McCune
ac2ff47a9c (#172) Wire Version Info in the UI
This patch adds the GetVersion rpc method to
holos.system.v1alpha1.SystemService and wires the version information up
to the Web UI.

This is a good example to crib from later regarding fetching and
refreshing data from the web ui using grpc and field masks.
2024-05-14 11:50:06 -07:00
Jeff McCune
9a2773c618 (#171) Refactor API to use FieldMasks
This patch refactors the API following the [API Best Practices][api]
documentation.  The UpdatePlatform method is modeled after a mutating
operation described [by Netflix][nflx] instead of using a REST resource
representation.  This makes it much easier to iterate over the fields
that need to be updated as the PlatformUpdateOperation is a flat data
structure while a Platform resource may have nested fields.  Nested
fields are more complicated and less clear to handle with a FieldMask.

This patch also adds a snapckbar message on save.  Previously, the save
button didn't give any indication of success or failure.  This patch
fixes the problem by adding a snackbar message that pop up at the bottom
of the screen nicely.

When the snackbar message is dismissed or times out the save button is
re-enabled.

[api]: https://protobuf.dev/programming-guides/api/
[nflx]: https://netflixtechblog.com/practical-api-design-at-netflix-part-2-protobuf-fieldmask-for-mutation-operations-2e75e1d230e4

Examples:

FieldMask for ListPlatforms

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ ${HOLOS_SERVER##*/} holos.platform.v1alpha1.PlatformService.ListPlatforms <<EOF
{
  "org_id": "018f36fb-e3f7-7f7f-a1c5-c85fb735d215",
  "field_mask": { "paths": ["id","name"] }
}
EOF
```

```json
{
 "platforms": [
   {
     "id": "018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94",
     "name": "bare"
   },
   {
     "id": "018f6b06-9e57-7223-91a9-784e145d998c",
     "name": "gary"
   },
   {
     "id": "018f6b06-9e53-7223-8ae1-1ad53d46b158",
     "name": "jeff"
   },
   {
     "id": "018f6b06-9e5b-7223-8b8b-ea62618e8200",
     "name": "nate"
   }
 ]
}
```

Closes: #171
2024-05-13 16:20:20 -07:00
Jeff McCune
51b6575d9f (#171) Refactor to API Best Practices
This patch refactors the API to be resource-oriented around one service
per resource type.  PlatformService, OrganizationService, UserService,
etc...

Validation is improved to use CEL rules provided by [protovalidate][1].

Place holders for FieldMask and other best practices are added, but are
unimplemented as per [API Best Practices][2].

The intent is to set us up well for copying and pasting solid existing
examples as we add features.

With this patch the server and web app client are both updated to use
the refactored API, however the following are not working:

 1. Update the model.
 2. Field Masks.

[1]: https://buf.build/bufbuild/protovalidate
[2]: https://protobuf.dev/programming-guides/api/
2024-05-10 15:55:41 -07:00
Jeff McCune
68a43f0682 (#167) Add holos rpc platform-model command
This command is just a prototype of how to fetch the platform model so
we can make it available to CUE.

The idea is we take the data from the holos server and write it into a
CUE `_Platform` struct.  This will probably involve converting the data
to CUE format and nesting it under the platform struct spec field.
2024-05-08 16:34:00 -07:00
Jeff McCune
9da88c4d1b (#169) ZITADEL ServerError - PGBouncer DNS
Add runbook notes.  Root cause and permanent solution have not been
identified yet.
2024-05-08 12:04:50 -07:00
Jeff McCune
19df2ec0fb (#167) Bump dev deployment to 0.74.0 2024-05-07 16:58:03 -07:00
Jeff McCune
bac7aec0ba (#167) Restructure the bare platform
This patch restructures the bare platform in preparation for a
`Platform` kind of output from CUE in addition to the existing
`BuildPlan` kind.

This patch establishes a pattern where our own CUE defined code goes
into the two CUE module paths:

1. `internal/platforms/cue.mod/gen/github.com/holos-run/holos/api/v1alpha1`
2. `internal/platforms/cue.mod/pkg/github.com/holos-run/holos/api/v1alpha1`
3. `internal/platforms/cue.mod/usr/github.com/holos-run/holos/api/v1alpha1`

The first path is automatically generated from Go structs.  The second
path is where we override and provide additional cue level integration.

The third path is reserved for the end user to further refine and
constrain our definitions.
2024-05-07 16:53:00 -07:00
Jeff McCune
42f916af41 (#164) Use quay.io/holos/oauth2-proxy:v7.6.0-1-g77a03ae2
Custom build to set samesite=none on the csrf cookie.
2024-05-06 16:18:32 -07:00
Jeff McCune
47a5e237e0 (#162) Lint go, typescript, and proto3 files
This patch adds lint coverage for proto3 and typescript to keep our code
reasonably clean.  The go linter was already enabled.
2024-05-06 14:17:08 -07:00
Jeff McCune
1279e2351a (#162) Move Platform back to holos.v1alpha1
No need to have a separate package for the PlatformService and related
protobuf messages.
2024-05-06 13:47:37 -07:00
Jeff McCune
adb8177026 Merge pull request #166 from holos-run/jeff/165-deploy-holos
(#165) Deploy Holos to Dev
2024-05-06 11:23:48 -07:00
Jeff McCune
4e8fa5abda (#165) Bump dev deployment to 0.73.1 2024-05-06 11:22:24 -07:00
Jeff McCune
6894f45b6c (#165) Deploy Holos to Dev
This patch deploys holos to the dev environment on the k2 cluster.  It's
accessible at https://app.dev.k2.holos.run/ behind the auth proxy by
default.
2024-05-06 11:10:29 -07:00
Jeff McCune
89d25be837 (#161) Wrap main router outlet in <main></main> 2024-05-06 09:16:15 -07:00
Jeff McCune
5b33e48552 (#161) Reasonably advanced form modeling the reference platform
This form goes a good way toward capturing what we need to configure the
entire reference platform.  Elements and sections are responsive to
which cloud providers are selected, which achieves my goal of modeling a
reasonably advanced form using only JSON data produced by CUE.

To write the form via the API:

    cue export ./forms/platform/ --out json \
      | jq '{platform_id: "'${platformId}'", fields: .spec.fields}' \
      | grpcurl -H "x-oidc-id-token: $(holos token)" -d @ ${host}:443 \
      holos.platform.v1alpha1.PlatformService.PutForm
2024-05-04 20:16:09 -07:00
Jeff McCune
79e8ab639a (#161) Fix the FormGroup & Refactor API
The way we were organizing fields into section broke Formly validation.
This patch fixes the problem by using the recommended approach of
[Nested Forms][1].

This patch also refactors the PlatformService API to clean it up.
GetForm / PutForm are separated from the Platform methods.  Similarly
GetModel / PutModel are separated out and are specific to get and put
the model data.

NOTE: I'm not sure we should have separated out the platform service
into it's own protobuf package.  Seems maybe unnecessary.

❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94"}' jeff.app.dev.k2.holos.run:443 holos.platform.v1alpha1.PlatformService.GetModel
{
  "model": {
    "org": {
      "contactEmail": "platform@openinfrastructure.co",
      "displayName": "Open Infrastructure Services LLC",
      "domain": "ois.run",
      "name": "ois"
    },
    "privacy": {
      "country": "earth",
      "regions": [
        "us-east-2",
        "us-west-2"
      ]
    },
    "terms": {
      "didAgree": true
    }
  }
}

[1]: https://formly.dev/docs/examples/other/nested-formly-forms
2024-05-04 10:14:37 -07:00
Jeff McCune
a0cc673736 (#150) Wire up select and multi select boxes
This patch wires up a Select and a Multi Select box.  This patch also
establishes a decision as it relates to Formly TypeScript / gRPC Proto3
/ CUE definitions of the form data structure.  The decision is to use
gRPC as a transport for any JSON to avoid friction trying to fit Formly
types into Proto3 messages.

Note when using google.protobuf.Value messages with bufbuild/connect-es,
we need to round trip them one last time through JSON to get the
original JSON on the other side.  This is because connect-es preserves
the type discriminators in the case and value fields of the message.

Refer to: [Accessing oneof
groups](https://github.com/bufbuild/protobuf-es/blob/main/docs/runtime_api.md#accessing-oneof-groups)

NOTE: On the wire, carry any JSON as field configs for expedience.  I
attempted to reflect FormlyFieldConfig in protobuf, but it was too time
consuming.  The loosely defined Formly json data API creates significant
friction when joined with a well defined protobuf API.  Therefore, we do
not specify anything about the Forms API, convey any valid JSON, and
leave it up to CUE and Formly on the sending and receiving side of the
API.

We use CUE to define our own holos form elements as a subset of the loose
Formly definitions.  We further hope Formly will move toward a better JSON
data API, but it's unlikely.  Consider replacing Formly entirely and
building on top of the strongly typed Angular Dyanmic Forms API.

Refer to: https://github.com/ngx-formly/ngx-formly/blob/v6.3.0/src/core/src/lib/models/fieldconfig.ts#L15
Consider: https://angular.io/guide/dynamic-form

Usage:

Generate the form from CUE

    cue export ./forms/platform/ --out json | jq -cM | pbcopy

Store the form JSON in the config_values column of the platforms table.

View the form, and submit some data. Then get the data back out for use rendering the platform:

    grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"'${platformId}'"}' $holos holos.v1alpha1.PlatformService.GetConfig

```json
{
  "platform": {
    "spec": {
      "config": {
        "user": {
          "sections": {
            "org": {
              "fields": {
                "contactEmail": "jeff@openinfrastructure.co",
                "displayName": "Open Infrastructure Services LLC",
                "domain": "ois.run",
                "name": "ois"
              }
            },
            "privacy": {
              "fields": {
                "country": "earth",
                "regions": [
                  "us-east-2",
                  "us-west-2"
                ]
              }
            },
            "terms": {
              "fields": {
                "didAgree": true
              }
            }
          }
        }
      }
    }
  }
}
```
2024-05-03 10:42:03 -07:00
Jeff McCune
d06ecfadc8 (#150) Refactor PlatformService.GetConfig for use with CUE
Problem:
The GetConfig response value isn't directly usable with CUE without some
gymnastics.

Solution:
Refactor the protobuf definition and response output to make the user
defined and supplied config values provided by the API directly usable
in the CUE code that defines the platform.

Result:

The top level platform config is directly usable in the
`internal/platforms/bare` directory:

    grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"'${platformID}'"}' $host \
      holos.v1alpha1.PlatformService.GetConfig \
      > platform.holos.json

Vet the user supplied data:

    cue vet ./ -d '#PlatformConfig' platform.holos.json

Build the holos component.  The ConfigMap consumes the user supplied
data:

    cue export --out yaml -t cluster=k2 ./components/configmap platform.holos.json \
      | yq .spec.components

Note the data provided by the input form is embedded into the
ConfigMap managed by Holos:

```yaml
KubernetesObjectsList:
  - metadata:
      name: platform-configmap
    apiObjectMap:
      ConfigMap:
        platform: |
          metadata:
            name: platform
            namespace: default
            labels:
              app.holos.run/managed: "true"
          data:
            platform: |
              kind: Platform
              spec:
                config:
                  user:
                    sections:
                      org:
                        fields:
                          contactEmail: jeff@openinfrastructure.co
                          displayName: Open Infrastructure Services LLC
                          domain: ois.run
                          name: ois
              apiVersion: app.holos.run/v1alpha1
              metadata:
                name: bare
                labels: {}
                annotations: {}
              holos:
                flags:
                  cluster: k2
          kind: ConfigMap
          apiVersion: v1
    Skip: false
```
2024-05-02 06:39:33 -07:00
Jeff McCune
64a117b0c3 (#150) Add PlatformService.GetConfig and refactor ConfigValues proto
Problem:
The use of google.protobuf.Any was making it awkward to work with the
data provided by the user.  The structure of the form data is defined by
the platform engineer, so the intent of Any was to wrap the data in a
way we can pass over the network and persist in the database.

The escaped JSON encoding was problematic and error prone to decode on
the other end.

Solution:
Define the Platform values as a two level map with string keys, but with
protobuf message fields "sections" and "fields" respectively.  Use
google.protobuf.Value from the struct package to encode the actual
value.

Result:
In TypeScript, google.protobuf.Value encodes and decodes easily to a
JSON value.  On the go side, connect correctly handles the value as
well.

No more ugly error prone escaping:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"'${platformId}'"}' $host holos.v1alpha1.PlatformService.GetConfig
{
  "sections": {
    "org": {
      "fields": {
        "contactEmail": "jeff@openinfrastructure.co",
        "displayName": "Open Infrastructure Services LLC",
        "domain": "ois.run",
        "name": "ois"
      }
    }
  }
}
```

This return value is intended to be directly usable in the CUE code, so
we may further nest the values into a platform.spec key.
2024-05-01 21:30:30 -07:00
Jeff McCune
cf006be9cf (#150) Add SystemService DropTables and SeedDatabase
Makes it easier to reset the database and give Gary and Nate access to
the same organization I'm in so they can provide feedback.
2024-05-01 14:30:13 -07:00
Jeff McCune
45ad3d8e63 (#150) Fix 500 error when config values aren't provided
AddPlatform was failing with a 500 error trying to decode a nil byte
slice when adding a platform without providing any values.
2024-05-01 11:31:25 -07:00
Jeff McCune
441c968c4f (#150) Look up user by iss sub, not email.
Also log when orgs are created.
2024-05-01 10:02:08 -07:00
Jeff McCune
99f2763fdf (#150) Store Platform Config Form and Values as JSON
This patch changes the backend to store the platform config form
definition and the config values supplied by the form as JSON in the
database.

The gRPC API does not change with this patch, but may need to depending
on how this works and how easy it is to evolve the data model and add
features.
2024-05-01 09:11:53 -07:00
Jeff McCune
1312395a11 (#150) Fix platforms page links
The links were hard to click.  This makes the links a much larger click
target following the example at https://material.angular.io/components/list/overview#navigation-lists
2024-05-01 08:51:29 -07:00
Jeff McCune
615f147bcb (#150) Add PutPlatformConfig to store the config values
This patch is a work in progress wiring up the form to put the values to
the holos server using grpc.

In an effort to simplify the platform configuration, the structure is a
two level map with the top level being configuration sections and the
second level being the fields associated with the config section.

To support multiple kinds of values and field controls, the values are
serialized to JSON for rpc over the network and for storage in the
database.  When they values are used, either by the UI or by the `holos
render` command, they're to be unmarshalled and in-lined into the
Platform Config data structure.

Pick back up ensuring the Platform rpc handler correctly encodes and
decodes the structure to the database.

Consider changing the config_form and config_values fields to JSON field
types in the database.  It will likely make working with this a lot
easier.

With this patch we're ready to wire up the holos render command to fetch
the platform configuration and create the end to end demo.

Here's essentially what the render command will fetch and lay down as a
json file for CUE:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f2c4e-ecde-7bcb-8b89-27a99e6cc7a1"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetPlatform | jq .platform.config.values
{
  "sections": {
    "org": {
      "values": {
        "contactEmail": "\"platform@openinfrastructure.co\"",
        "displayName": "\"Open Infrastructure Services  LLC\"",
        "domain": "\"ois.run\"",
        "name": "\"ois\""
      }
    }
  }
}
```
2024-04-30 20:21:15 -07:00
Jeff McCune
d0ad3bfc69 (#150) Add Platform Detail to edit platform config
This patch adds a /platform/:id route path to a PlatformDetail
component.  The platform detail component calls the GetPlatform method
given the platform ID and renders the platform config form on the detail
tab.

The submit button is not yet wired up.

The API for adding platforms changes, allowing raw json bytes using the
RawConfig.  The raw bytes are not presented on the read path though,
calling GetPlatforms provides the platform and the config form inline in
the response.

Use the `raw_config` field instead of `config` when creating the form
data.

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare2",
    "raw_config": {
      "form": "$(cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```
2024-04-30 14:02:49 -07:00
Jeff McCune
fe58a33747 (#150) Add holos.v1alpha1.PlatformService.GetForm
The GetForm method is intended for the Angular frontend to get
[FormlyFieldConfig][1] data for each section of the Platform config.

[1]: https://formly.dev/docs/api/core/#formlyfieldconfig

Steps to exercise for later testing:

Add the form definition to the database:

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare${RANDOM}",
    "config": {
      "form": "$(cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```

Get the form definition back out:

```

❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f2bc1-6590-7670-958a-9f3bc02b658f"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetForm
{
  "apiVersion": "forms.holos.run/v1alpha1",
  "kind": "PlatformForm",
  "metadata": {
    "name": "bare"
  },
  "spec": {
    "sections": [
      {
        "name": "org",
        "displayName": "Organization",
        "description": "Organization config values are used to derive more specific configuration values throughout the platform.",
        "fieldConfigs": [
          {
            "key": "name",
            "type": "input",
            "props": {
              "label": "Name",
              "placeholder": "example",
              "description": "DNS label, e.g. 'example'",
              "required": true
            }
          },
          {
            "key": "domain",
            "type": "input",
            "props": {
              "label": "Domain",
              "placeholder": "example.com",
              "description": "DNS domain, e.g. 'example.com'",
              "required": true
            }
          },
          {
            "key": "displayName",
            "type": "input",
            "props": {
              "label": "Display Name",
              "placeholder": "Example Organization",
              "description": "Display name, e.g. 'Example Organization'",
              "required": true
            }
          },
          {
            "key": "contactEmail",
            "type": "input",
            "props": {
              "label": "Contact Email",
              "placeholder": "platform-team@example.com",
              "description": "Technical contact email address",
              "required": true
            }
          }
        ]
      }
    ]
  }
}
```

References

```
❯ cue export ./forms/platform/ --out yaml | yq
apiVersion: forms.holos.run/v1alpha1
kind: PlatformForm
metadata:
  name: bare
spec:
  sections:
    - name: org
      displayName: Organization
      description: Organization config values are used to derive more specific configuration values throughout the platform.
      fieldConfigs:
        - key: name
          type: input
          props:
            label: Name
            placeholder: example
            description: DNS label, e.g. 'example'
            required: true
        - key: domain
          type: input
          props:
            label: Domain
            placeholder: example.com
            description: DNS domain, e.g. 'example.com'
            required: true
        - key: displayName
          type: input
          props:
            label: Display Name
            placeholder: Example Organization
            description: Display name, e.g. 'Example Organization'
            required: true
        - key: contactEmail
          type: input
          props:
            label: Contact Email
            placeholder: platform-team@example.com
            description: Technical contact email address
            required: true
```
2024-04-29 14:24:16 -07:00
Jeff McCune
26e537e768 (#150) Add platform config form, values, cue
This patch adds 4 fields to the Platform table:

 1. Config Form represents the JSON FormlyFieldConfig for the UI.
 2. Config CUE represents the CUE file containing a definition the
    Config Values must unify with.
 3. Config Definition is the CUE definition variable name used to unify
    the values with the cue code.  Should be #PlatformSpec in most
    cases.
 4. Config Values represents the JSON values provided by the UI.

The use case is the platform engineer defines the #PlatformSpec in cue,
and provides the form field config.  The platform engineer then provides
1-3 above when adding or updating a Platform.

The UI then presents the form to the end user and provides values for 4
when the user submits the form.

This patch also refactors the AddPlatform method to accept a Platform
message.  To do so we make the id field optional since it is server
assigned.

The patch also adds a database constraint to ensure platform names are
unique within the scope of an organization.

Results:

Note how the CUE representation of the Platform Form is exported to JSON
then converted to a base64 encoded string, which is the protobuf JSON
representation of a bytes[] value.

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "id": "0d3dc0c0-bbc8-41f8-8c6e-75f0476509d6",
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare",
    "config": {
      "form": "$(cd internal/platforms/bare && cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```

Note the requested platform ID is ignored.

```
{
  "platforms": [
    {
      "id": "018f2af9-f7ba-772a-9db6-f985ece8fed1",
      "timestamps": {
        "createdAt": "2024-04-29T17:49:36.058379Z",
        "updatedAt": "2024-04-29T17:49:36.058379Z"
      },
      "name": "bare",
      "creator": {
        "id": "018f27cd-e591-7f98-a9d2-416167282d37"
      },
      "config": {
        "form": "eyJhcGlWZXJzaW9uIjoiZm9ybXMuaG9sb3MucnVuL3YxYWxwaGExIiwia2luZCI6IlBsYXRmb3JtRm9ybSIsIm1ldGFkYXRhIjp7Im5hbWUiOiJiYXJlIn0sInNwZWMiOnsic2VjdGlvbnMiOlt7Im5hbWUiOiJvcmciLCJkaXNwbGF5TmFtZSI6Ik9yZ2FuaXphdGlvbiIsImRlc2NyaXB0aW9uIjoiT3JnYW5pemF0aW9uIGNvbmZpZyB2YWx1ZXMgYXJlIHVzZWQgdG8gZGVyaXZlIG1vcmUgc3BlY2lmaWMgY29uZmlndXJhdGlvbiB2YWx1ZXMgdGhyb3VnaG91dCB0aGUgcGxhdGZvcm0uIiwiZmllbGRDb25maWdzIjpbeyJrZXkiOiJuYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJOYW1lIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlIiwiZGVzY3JpcHRpb24iOiJETlMgbGFiZWwsIGUuZy4gJ2V4YW1wbGUnIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRvbWFpbiIsInR5cGUiOiJpbnB1dCIsInByb3BzIjp7ImxhYmVsIjoiRG9tYWluIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlLmNvbSIsImRlc2NyaXB0aW9uIjoiRE5TIGRvbWFpbiwgZS5nLiAnZXhhbXBsZS5jb20nIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRpc3BsYXlOYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJEaXNwbGF5IE5hbWUiLCJwbGFjZWhvbGRlciI6IkV4YW1wbGUgT3JnYW5pemF0aW9uIiwiZGVzY3JpcHRpb24iOiJEaXNwbGF5IG5hbWUsIGUuZy4gJ0V4YW1wbGUgT3JnYW5pemF0aW9uJyIsInJlcXVpcmVkIjp0cnVlfX0seyJrZXkiOiJjb250YWN0RW1haWwiLCJ0eXBlIjoiaW5wdXQiLCJwcm9wcyI6eyJsYWJlbCI6IkNvbnRhY3QgRW1haWwiLCJwbGFjZWhvbGRlciI6InBsYXRmb3JtLXRlYW1AZXhhbXBsZS5jb20iLCJkZXNjcmlwdGlvbiI6IlRlY2huaWNhbCBjb250YWN0IGVtYWlsIGFkZHJlc3MiLCJyZXF1aXJlZCI6dHJ1ZX19XX1dfX0K"
      }
    }
  ]
}
```
2024-04-29 10:53:23 -07:00
Jeff McCune
ad70a6c4fe (#150) Add holos.v1alpha1.PlatformService.AddPlatform
This patch adds a basic AddPlatform method that adds a platform with a
name and a display name.

Next steps are to add fields for the Platform Config Form definition and
the Platform Config values submitted from the form.
2024-04-29 09:35:49 -07:00
Jeff McCune
22a04da6bb (#150) Add holos.v1alpha1.PlatformService.GetPlatforms
Next step: AddPlatform

Also consider extracting the queries to get the requested org_id to a
helper function.  This will likely eventually move to an interceptor
because every request is org scoped and needs authorization checks
against the org.

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"org_id":"018f27cd-e5ac-7f98-bfe1-2dbab208a48c"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetPlatforms
```
2024-04-28 20:21:32 -07:00
Jeff McCune
dc97fe0ff0 (#150) Define a PlatformForm for platform design
Problem:
Platform engineers need the ability to define custom input fields for
their own platform level configuration values.  The holos web UI needs
to present the platform config values in a clean way.  The values
entered on the form need to make their way into the top level
Platform.spec field for use across all components and clusters in the
platform.

Solution:
Define a Platform Form in a forms cue package.  The output of this
definition is intended to be sent to the holos server to provide to the
web UI.

Result:
Platform engineers can define their platform config input values in
their infrastructure repository.  For example, the bare platform form
inputs are defined at `platforms/bare/forms/platform/platform-form.cue`.

This cue file produces [FormlyFieldConfig][1] output.

```console
cue export ./forms/platform/ --out yaml
```

```yaml
apiVersion: forms.holos.run/v1alpha1
kind: PlatformForm
metadata:
  name: bare
spec:
  sections:
    - name: org
      displayName: Organization
      description: Organization config values are used to derive more specific configuration values throughout the platform.
      fieldConfigs:
        - key: name
          type: input
          props:
            label: Name
            placeholder: example
            description: DNS label, e.g. 'example'
            required: true
        - key: domain
          type: input
          props:
            label: Domain
            placeholder: example.com
            description: DNS domain, e.g. 'example.com'
            required: true
        - key: displayName
          type: input
          props:
            label: Display Name
            placeholder: Example Organization
            description: Display name, e.g. 'Example Organization'
            required: true
        - key: contactEmail
          type: input
          props:
            label: Contact Email
            placeholder: platform-team@example.com
            description: Technical contact email address
            required: true
```

Next Steps:
Add a holos subcommand to produce the output and store it in the
backend.  Wire the front end to fetch the form config from the backend.

[1]: https://formly.dev/docs/api/core#formlyfieldconfig
2024-04-28 11:25:06 -07:00
Jeff McCune
9ca97c6e01 Merge pull request #148 from holos-run/jeff/147-cue-oom
(#147) Add holos render --print-instances flag
2024-04-26 16:31:14 -07:00
Jeff McCune
924653e240 (#150) Bare Platform
This patch adds a bare platform that does nothing but render a configmap
containing the platform config structure itself.

The definition of the platform structure is firming up.  The platform
designer, which may be a holos customer, is responsible for defining the
structure of the `platform.spec` output field.

Us holos developers have a reserved namespace to add configuration
fields and data in the `platform.holos` output file.

Beyond these two fields, the platform config structure has TypeMeta and
ObjectMeta fields similar to a kubernetes api object to support
versioning the platform config data, naming the platform, annotating the
platform, and labeling the platform.

The path forward from here is to:

 1. Eventually move the stable definitions into a CUE module that gets
    imported into the user's package.
 2. As a platform designer, add the organization field to the
    #PlatformSpec definition as a CUE definition.
 3. As a platform designer, add the organization field Form data
    structure as a JSON file.
 4. Add an API to upload the #PlatformSpec cue file and the
    #PlatformSpec form json file to the saas backend.
 5. Wire up Angular to pull the form json from the API and render the
    form.
 6. Wire up Angular to write the form data to a gRPC service method.
 7. Wire up the `holos cli` to read the form data from a gRPC service
    method.
 8. Tie it all together where the holos cli renders the configmap.
2024-04-26 16:14:30 -07:00
Jeff McCune
59d48f8599 (#146) Platform Config Mock Up
This patch adds a mock up of the platform config.  The goal is to use
this to connect to an anemic example platform built from `holos init`.
2024-04-26 11:29:58 -07:00
Jeff McCune
90f8eab816 (#144) Tidy go.mod and package.json 2024-04-25 19:14:20 -07:00
Jeff McCune
9ae45e260d (#147) Add holos render --print-instances flag
To enumerate all of the instances that could be run in separate
processes with xargs instead of run in the for loop in the Builder Run
method.
2024-04-25 13:59:10 -07:00
Jeff McCune
aee15f95e2 Merge pull request #145 from holos-run/jeff/144-organization-selector
(#144) Profile Button
2024-04-25 09:55:55 -07:00
Jeff McCune
1c540ac375 (#144) Profile Button and Organization Selector
This patch adds an organization "selector" that's really just a place
holder.  The active organization is the last element in the list
returned by the GetCallerOrganizations method for now.

The purpose is to make sure we have the structure in place for more than
one organizations without needing to implement full support for the
feature at this early stage.

The Angular frontend is expected to call the activeOrg() method of the
OrganizationService.  In the future this could store the state of which
organization the user has selected.  The purpose is to return an org id
to send as a request parameter for other requests.

Note this patch also implements refresh behavior.  The list of orgs is
fetched once on application load.  If there is no user, or the user has
zero orgs, the user is created and an organization is added with them as
an owner.  This is accompished using observable pipes.

The pipe is tied to a refresh behavior.  Clicking the org button
triggers the refresh behavior, which executes the pipe again and
notifies all subscribers.

This works quite well and should be idiomatic angular / rxjs.  Clicking
the button automatically updates the UI after making the necessary API
calls.
2024-04-25 09:55:13 -07:00
Jeff McCune
5b0e883ac9 (#144) Get or Create the orgranization
This patch adds the OrganizationService to the Angular front end and
displays a simple list of the organizations the user is a member of in
the profile card.

There isn't a service yet to return the currently selected
organization, but that could be a simple method to return the most
recent entry in the list until we put something more complicated in
place like local storage of what the user has selected.

It may make sense to put a database constraint on the number of
organizations until we implement the feature later, it's too early to do
so now, I just want to make sure it's possible to add later.
2024-04-25 07:02:17 -07:00
Jeff McCune
9a2519af71 (#144) Make the linter happy 2024-04-24 13:41:45 -07:00
Jeff McCune
9b9ff601c0 (#144) Call GetCallerClaims once instead of multiple times
Problem:
When loading the page the GetCallerClaims rpc method is called multiple
times unnecessarily.

Solution:
Use [shareReplay][1] to replay the last observable event for all
subscribers, including subscribers coming late to the party.

Result:
Network inspector in chrome indicates GetCallerClaims is called once and
only once.

[1]: https://rxjs.dev/api/operators/shareReplay
2024-04-24 12:44:44 -07:00
Jeff McCune
2f798296dc (#144) Profile Button
This patch adds a ProfileButton component which makes a ConnectRPC gRPC
call to the `holos.v1alpha1.UserService.GetCallerClaims` method and
renders the profile button based on the claims.

Note, in the network inspector there are two API calls to
`holos.v1alpha1.UserService.GetCallerClaims` which is unfortunate.  A
follow up patch might be good to fix this.
2024-04-24 12:23:54 -07:00
Jeff McCune
2b2ff63cad (#144) Connect /ui to ng serve for hot reload
Problem:
It's slow to build the angular app, compile it into the go executable,
copy it to the pod, then restart the server.

Solution:
Configure the mesh to route /ui to `ng serve` running on my local
host.

Result:
Navigating to https://jeff.app.dev.k2.holos.run/ui gets responses from
the ng development server.

Use:

    ng serve --host 0.0.0.0
2024-04-23 20:30:02 -07:00
Jeff McCune
3b135c09f3 (#144) Make a ConnectRPC call to the GetUserClaims method
This patch wires up an Angular RxJS Observable to the result of a gRPC
call to the `holos.v1alpha1.UserService.GetCallerClaims` method.

The implementation is a combination of [this connect example][1] and the
official [angular data][2] guide.

[1]: https://github.com/connectrpc/examples-es/tree/main/angular
[2]: https://angular.io/start/start-data#configuring-the-shippingcomponent-to-use-cartservice
2024-04-23 17:18:35 -07:00
Jeff McCune
28813eba5b (#126) v0.69.0 2024-04-23 10:24:58 -07:00
Jeff McCune
02ff765f54 Merge pull request #143 from holos-run/jeff/126-registration
(#126) Minimal API to register users and organizations
2024-04-23 09:00:07 -07:00
Jeff McCune
fe8a806132 (#126) Refactor to GetCallerX / CreateCallerX
This patch simplifies the user and organization registration and query
for the UI.  The pattern clients are expected to follow is to create if
the get fails.  For example, the following pseudo-go-code is the
expected calling convention:

    var entity *ent.User
    entity, err := Get()
    if err != nil {
      if ent.MaskNotFound(err) == nil {
        entity = Create()
      } else {
        return err
      }
    }
    return entity

This patch adds the following service methods.  For initial
registration, all input data comes from the id token claims of the
authenticated user.

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list | xargs -n1 grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list
holos.v1alpha1.OrganizationService.CreateCallerOrganization
holos.v1alpha1.OrganizationService.GetCallerOrganizations
holos.v1alpha1.UserService.CreateCallerUser
holos.v1alpha1.UserService.GetCallerClaims
holos.v1alpha1.UserService.GetCallerUser
```
2024-04-23 08:43:23 -07:00
Jeff McCune
6626d58301 (#126) Add OrganizationService
Next step after this is to simplify the calling convention to a get
followed by a create if the get fails.
2024-04-23 05:28:07 -07:00
Jeff McCune
cb0911e890 (#126) Add holos.v1alpha1.UserService.GetUser
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.UserService.GetUser
{
  "user": {
    "id": "018f07f4-4f9c-7b69-9d9e-07bf7bb4fe33",
    "email": "jeff@openinfrastructure.co",
    "name": "Jeff McCune",
    "timestamps": {
      "createdAt": "2024-04-22T22:36:42.780492Z",
      "updatedAt": "2024-04-22T22:36:42.780492Z"
    }
  }
}
2024-04-22 16:49:25 -07:00
Jeff McCune
3745a68dc5 (#126) Add unique index on user iss sub fields
The server will frequently look up the user record given the iss and sub
claims from the id token, index them and make sure the combination of
the two is unique.
2024-04-22 16:14:40 -07:00
Jeff McCune
fd64830476 (#126) Rename HolosService to UserService
Organize services by the resource they manage.
2024-04-22 16:05:32 -07:00
Jeff McCune
1ee0fa9c1f (#126) User Registration via API
With this patch user registration works with grpcurl.  Nothing in the
web UI yet.

```
grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.RegisterUser
```

Cannot register twice:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.RegisterUser
ERROR:
  Code: FailedPrecondition
  Message: user.go:26: ent: constraint failed: ERROR: duplicate key value violates unique constraint "users_email_key" (SQLSTATE 23505)
```

GetUserClaims works though:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 holos.v1alpha1.HolosService.GetUserClaims
{
  "iss": "https://login.ois.run",
  "sub": "261773693724656988",
  "email": "jeff@openinfrastructure.co",
  "emailVerified": true,
  "name": "Jeff McCune"
}
```
2024-04-22 15:38:07 -07:00
Jeff McCune
8fab325b0a (#126) Add gRPC reflection
So grpcurl works as expected:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" jeff.app.dev.k2.holos.run:443 list
holos.v1alpha1.HolosService
```
2024-04-22 15:02:08 -07:00
Jeff McCune
858ffad913 (#126) Add holos token command for grpcurl 2024-04-22 14:54:09 -07:00
Jeff McCune
62735b99e7 (#126) Update Tiltfile to use holos.run for dev
This patch updates the Tiltfile to use the holos.run domain which is
integrated with the default Gateway.
2024-04-22 13:42:18 -07:00
Jeff McCune
29ab9c6300 (#141) Install provisioner helper.rb from entrypoint
And add a script to reset the choria provisioner credentials and config.
2024-04-22 13:20:38 -07:00
Jeff McCune
debc01c7de (#141) Fix Incorrect Provisioning Token foo given
The `make-provisioner-jwt` incorrectly used the choria broker password
as the provisioning token.  In the reference [setup.sh][1] both the
token and the `broker_provisioning_password` are set to `s3cret` so I
confused the two, but they are actually different values.

This patch ensures the provisioning token configured in
`provisioner.yaml` matches the token embedded into the provisioning.jwt
file using `choria jwt provisioning` via the `make-provisioner-jwt`
script.

[1]: 6dbc8fd105/example/setup/templates/provisioner/provisioner.yaml (L6)
2024-04-22 12:31:10 -07:00
Jeff McCune
c07f35ecd6 (#141) Fix holos controller invalid websocket connection error
Problem:
When the ingress default Gateway AuthorizationPolicy/authpolicy-custom
rule is in place the choria machine room holos controller fails to
connect to the provisioner broker with the following error:

```
❯ holos controller run --config=agent.cfg
WARN[0000] Starting controller version 0.68.1 with config file /home/jeff/workspace/holos-run/holos/hack/choria/agent/agent.cfg  leader=false
WARN[0000] Switching to provisioning configuration due to build defaults and missing /home/jeff/workspace/holos-run/holos/hack/choria/agent/agent.cfg
WARN[0000] Setting anonymous TLS mode during provisioning  component=server connection=coffee.home identity=coffee.home
WARN[0000] Initial connection to the Broker failed on try 1: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
WARN[0000] Initial connection to the Broker failed on try 2: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
WARN[0002] Initial connection to the Broker failed on try 3: invalid websocket connection  component=server connection=coffee.home identity=coffee.home
```

This problem is caused because the provisioning token url is set to
`wss://jeff.provision.dev.k2.holos.run:443` which has the port number
specified.

Solution:
Follow the upstream istio guidance of [Writing Host Match Policies][1]
to match host headers with or without the port specified.

Result:
The controller is able to connect to the provisioner broker:

[1]: https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
2024-04-22 12:31:10 -07:00
Jeff McCune
c8f528700c (#141) Fix error: do not know how to handle choria_provisioning purpose token
Solution:
remove the plugin.security.choria.ca setting
2024-04-22 12:30:16 -07:00
Jeff McCune
896248c237 (#141) Try and connect holos controller to the provisioner
Running into error:

time="2024-04-20T03:23:19Z" level=warning msg="Denying connection: verified error: do not know how to handle choria_provisioning purpose token, unverified error: <nil>" component=authentication remote="10.244.1.51:56338" stage=check
time="2024-04-20T03:23:19Z" level=error msg="192.168.2.21/10.244.1.51:56338 - wid:367 - authentication error" component=network_broker
2024-04-22 12:29:56 -07:00
Jeff McCune
74a181db21 (#133) Add missing Choria Provisioner deployment 2024-04-19 15:14:31 -07:00
Jeff McCune
ba10113342 (#133) Fix tls error when connecting to provisioner websocket
This problem fixes an error where the istio ingress gateway proxy failed
to verify the TLS certificate presented by the choria broker upstream
server.

    kubectl logs choria-broker-0

    level=error msg="websocket: TLS handshake error from 10.244.1.190:36142: remote error: tls: unknown certificate\n"

Istio ingress logs:

    kubectl -n istio-ingress logs -l app=istio-ingressgateway -f | grep --line-buffered '^{' | jq .

    "upstream_transport_failure_reason": "TLS_error:|268435581:SSL_routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED:TLS_error_end:TLS_error_end"

Client curl output:

    curl https://jeff.provision.dev.k2.holos.run

    upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: TLS_error:|268435581:SSL routines:OPENSSL_i
nternal:CERTIFICATE_VERIFY_FAILED:TLS_error_end:TLS_error_end

Explanation of error:

Istio defaults to expecting a tls certificate matching the downstream
host/authority which isn't how we've configured Choria.

Refer to [ClientTLSSettings][1]

> A list of alternate names to verify the subject identity in the
> certificate. If specified, the proxy will verify that the server
> certificate’s subject alt name matches one of the specified values. If
> specified, this list overrides the value of subject_alt_names from the
> ServiceEntry. If unspecified, automatic validation of upstream presented
> certificate for new upstream connections will be done based on the
> downstream HTTP host/authority header, provided
> VERIFY_CERTIFICATE_AT_CLIENT and ENABLE_AUTO_SNI environmental variables
> are set to true.

[1]: https://istio.io/latest/docs/reference/config/networking/destination-rule/#ClientTLSSettings
2024-04-19 13:13:09 -07:00
Jeff McCune
eb0207c92e (#133) Choria Provisioner
This patch is a work in progress to configure the provisioner to connect
to the broker.  Services and deployments are prefixed with choria for
clarity.
2024-04-19 13:13:08 -07:00
Jeff McCune
0fbcee8119 (#133) Extend the life of the Platform Issuer CA
The platform issuer root CA was set to expire after 90 days, the default
value.  This is too short.

Extend the life of the root CA beyond 100 years.
2024-04-19 11:29:17 -07:00
Jeff McCune
ce8bc798f6 (#133) Exclude nats and provision hosts from the auth proxy
Problem:
The identity aware auth proxy attached to the default gateway is
blocking access to NATS and the Choria Provisioner cluster.

Solution:
Add configuration that causes the project hosts to get added to the
exclusion list of the AuthorizationPolicy/authproxy-custom rule.

Result:
Requests bypass the auth proxy and go straight to the backend.  The
rules look like:

    kubectl get authorizationpolicy authproxy-custom -o yaml

```yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: authproxy-custom
  namespace: istio-ingress
  labels:
    app.kubernetes.io/name: authproxy-custom
    app.kubernetes.io/part-of: istio-ingressgateway
spec:
  action: CUSTOM
  provider:
    name: ingressauth
  rules:
  - to:
    - operation:
        notHosts:
        - login.ois.run
        - vault.core.ois.run
        - provision.holos.run
        - nats.holos.run
        - provision.dev.holos.run
        - nats.dev.holos.run
        - jeff.provision.dev.holos.run
        - jeff.nats.dev.holos.run
        - gary.provision.dev.holos.run
        - gary.nats.dev.holos.run
        - nate.provision.dev.holos.run
        - nate.nats.dev.holos.run
        - provision.k2.holos.run
        - nats.k2.holos.run
        - provision.dev.k2.holos.run
        - nats.dev.k2.holos.run
        - jeff.provision.dev.k2.holos.run
        - jeff.nats.dev.k2.holos.run
        - gary.provision.dev.k2.holos.run
        - gary.nats.dev.k2.holos.run
        - nate.provision.dev.k2.holos.run
        - nate.nats.dev.k2.holos.run
    when:
    - key: request.headers[x-oidc-id-token]
      notValues:
      - '*'
  selector:
    matchLabels:
      istio: ingressgateway
```
2024-04-19 06:32:11 -07:00
Jeff McCune
996195d651 (#137) Fix ArgoCD PKCE login small comment 2024-04-18 14:39:13 -07:00
Jeff McCune
f00b29d3a3 (#137) Fix ArgoCD PKCE login
This patch configures ArgoCD to log in via PKCE.

Note the changes are primarily in platform.site.cue and ensuring the
emailDomain is set properly.  Note too the redirect URL needs to be
`/pkce/verify` when PKCE is enabled.  Finally, if the setting is
reconfigured make sure to clear cookies otherwise the incorrect
`/auth/callback` path may be used.
2024-04-18 14:37:06 -07:00
Jeff McCune
a6756ecf11 (#132) Update go deps to fix Machine Room 2024-04-18 13:35:58 -07:00
Jeff McCune
ef7ec30037 (#132) Use machine-room main branch
Our patch to use Args got merged.
2024-04-18 11:38:51 -07:00
Jeff McCune
1642787825 (#101) Fix port names in servers must be unique: duplicate name https
Problem:
Port names in the default Gateway.spec.servers.port field must be unique
across all servers associated with the workload.

Solution:
Append the fully qualified domain name with dots replaced with hyphens.

Result:
Port name is unique.
2024-04-18 11:21:05 -07:00
Jeff McCune
f83781480f (#101) Do not add Gateway.spec.servers for other clusters
Problem:
The default gateway in one cluster gets server entries for all hosts in
the problem.  This makes the list unnecessarily large with entries for
clusters that should not be handled on the current cluster.

For example, the k2 cluster has gateway entries to route hosts for k1,
k3, k4, k5, etc...

Solution:
Add a field to the CertInfo definition representing which clusters the
host is valid on.

Result:
Hosts which are valid on all clusters, e.g. login.ois.run, have all
project clusters added to the clusters field of the CertInfo.  Hosts
which are valid on a single cluster have the coresponding single entry
added.

When building resources, holos components should check if `#ClusterName`
is a valid field of the CertInfo.clusters field.  If so, the host is
valid for the current cluster.  If not, the host should be omitted from
the current cluster.
2024-04-18 11:10:16 -07:00
Jeff McCune
9b70205855 (#101) Use letsencrypt production instead of staging
Certificates issue OK from staging, switching to production.
2024-04-18 10:36:28 -07:00
Jeff McCune
0e4bf3c144 (#101) Manage certs on all clusters
We're going to do a big re-issue so might as well do it once so we don't
have to re-issue again to add more clusters to existing projects.
2024-04-18 10:16:55 -07:00
Jeff McCune
1241c74b41 (#101) Do not add the project name as a project host
Doing so forces unnecessary hosts for some projects.  For example,
iam.ois.run is useless for the iam project, the primary project host is
login to build login.ois.run.

Some projects may not need any hosts as well.

Better to let the user specify `project: foo: hosts: foo: _` if they
want it.
2024-04-18 09:59:21 -07:00
Jeff McCune
44fea098de (#101) Manage an ExternalSecret for every Server in the default Gateway
This patch loops over every Gateway.spec.servers entry in the default
gateway and manages an ExternalSecret to sync the credential from the
provisioner cluster.
2024-04-18 09:53:39 -07:00
Jeff McCune
52286efa25 (#101) Fix duplicate certs in holos components
Problem:
A Holos Component is created for each project stage, but all hosts for
all stages in the project are added.  This creates duplicates.

Solution:
Sort project hosts by their stage and map the holos component for a
stage to the hosts for that stage.

Result:
Duplicates are eliminated, the prod certs are not in the dev holos
component and vice-versa.
2024-04-18 09:17:49 -07:00
Jeff McCune
a1b2179442 (#101) Remove holos-saas-certs holos component
No longer needed now that project host certs are using wildcards and
organized nicely.
2024-04-18 06:32:06 -07:00
Jeff McCune
cffc430738 (#101) Provision wildcard certs for all Gateway servers
This patch provisions wildcard certs in the provisioning cluster.  The
CN matches the project stage host global hostname without any cluster
qualifiers.

The use of a wildcard in place of the environment name dns segment at
the leftmost position of the fully qualified dns name enables additional
environments to be configured without reissuing certificates.

This is to avoid the 100 name per cert limit in LetsEncrypt.
2024-04-18 06:26:29 -07:00
Jeff McCune
d76454272b (#101) Simplify the GatewayServers struct
Mapping each project host fqdn to the stage is unnecessary.  The list of
gateway servers is constructed from each FQDN in the project.

This patch removes the unnecessary struct mappings.
2024-04-18 05:32:19 -07:00
Jeff McCune
9d1e77c00f (#101) Define #ProjectHosts to manage project hosts
Problem:
It's difficult to map and reduce the collection of project hosts when
configuring related Certificate, Gateway.spec.servers, VirtualService,
and auth proxy cookie domain settings.

Solution:
Define #ProjectHosts which takes a project and provides Hosts which is a
struct with a fqdn key and a #CertInfo value.  The #CertInfo definition
is intended to provide everything need to reduce the Hosts property to
structs usful for the problematic resources mentioned previously.

Result:
Gateway.spec.servers are mapped using #ProjectHosts

Next step is to map the Certificate resources on the provisioner
cluster.
2024-04-17 21:59:04 -07:00
Jeff McCune
2050abdc6c (#101) Add wildcard support to project certs
Problem:
Adding environments to a project causes certs to be re-issued.

Solution:
Enable wildcard certs for per-environment namespaces like jeff, gary,
nate, etc...

Result:
Environments can be added to a project stage without needing the cert to
be re-issued.
2024-04-17 12:32:44 -07:00
Jeff McCune
3ea013c503 (#101) Consolidate certificates by project stage
This patch avoids LetsEncrypt rate limits by consolidating multiple dns
names into one certificate.

For each project host, create a certificate for each stage in the
project.  The certificate contains the dns names for all clusters and
environments associated with that stage and host.

This can become quite a list, the limit is 100 dnsNames.

For the Holos project which has 7 clusters and 4 dev environments, the
number of dns names is 32 (4 envs + 4 envs * 7 clusters = 32 dns names).

Still, a much needed improvement because we're limited to 50 certs per
week.

It may be worth considering wildcards for the per-developer
environments, which are the ones we'll likely spin up the most
frequently.
2024-04-17 11:58:46 -07:00
Jeff McCune
309db96138 (#133) Choria Broker for Holos Controller provisioning
This patch is a partial step toward getting the choria broker up
and running in my own namespace.  The choria broker is necessary for
provisioning machine room agents such as the holos controller.
2024-04-17 08:48:31 -07:00
Jeff McCune
283b4be71c (#132) Use forked version of machine-room
Until https://github.com/choria-io/machine-room/pull/12 gets merged
2024-04-16 19:46:36 -07:00
Jeff McCune
ab9bca0750 (#132) Controller Subcommand
This patch adds an initial holos controller subcommand.  The machine
room agent starts, but doesn't yet provision because we haven't deployed
the provisioning infrastructure yet.
2024-04-16 15:40:25 -07:00
Jeff McCune
ac2be67c3c (#130) NATS deployment with operator jwt
Configure NATS in a 3 Node deployment with resolver authentication using
an Operator JWT.

The operator secret nkeys are stored in the provisioner cluster.  Get
them with:

    holos get secret -n jeff-holos nats-nsc --print-key nsc.tgz | tar -tvzf-
2024-04-15 17:02:18 -07:00
Jeff McCune
6ffafb8cca (#127) Setup Routing using Dashboard Schematic
This patch sets up basic routing and a 404 not found page.  The Home and
Clusters page are generated from the [dashboard schematic][1]

    ng generate @angular/material:dashboard home
    ng generate @angular/material:dashboard cluster-list
    ng g c error-not-found

[1]: https://material.angular.io/guide/schematics#dashboard-schematic
2024-04-15 13:48:00 -07:00
Jeff McCune
590e6b556c (#127) Generate Angular Material navigation
Instead of trying to hand-craft a navigation sidebar and toolbar from
Youtube videos, use the [navigation schematic][1] to quickly get a "good
enough" UI.

    ng generate @angular/material:navigation nav

[1]: https://material.angular.io/guide/schematics#navigation-schematic
2024-04-15 10:43:24 -07:00
Jeff McCune
5dc5c6fbdf (#127) ng add @angular/material
And start working on the sidenav and toolbar.
2024-04-14 07:03:45 -07:00
Jeff McCune
cd8c9f2c32 (#127) ConnectRPC generated code 2024-04-13 11:03:19 -07:00
Jeff McCune
3490941d4c (#127) Frontend deps from make tools
Needed to generate the connectrpc bindings and build the holos
executable.
2024-04-12 20:09:41 -07:00
Jeff McCune
3f201df0c2 (#126) Configure Angular to align with frontend.go
Angular must build output into a path compatible with the Go
http.FileServer.  We cannot easily graft an fs.FS onto a sub-path, so we
need the `./ui/` path in the output.  This requires special
configuration from the Angular default application builder behavior.
2024-04-12 20:08:37 -07:00
Jeff McCune
4c22d515bd (#127) ng new holos
ng new holos --routing --skip-git --standalone
SCSS
No SSR
2024-04-12 20:07:17 -07:00
Jeff McCune
ec0ef1c4b3 (#127) Angular - Restart again
Restart again this time with SCSS instead of CSS.
2024-04-12 20:03:45 -07:00
Jeff McCune
1e51e2d49a (#127) Angular Navigation schematic
Following [Navigation schematic][1].

    ng generate @angular/material:navigation navigation

[1]: https://material.angular.io/guide/schematics#navigation-schematic
2024-04-12 19:45:26 -07:00
Jeff McCune
5186499b90 Revert "(#127) Angular - ng add ng-matero"
This reverts commit fc275e4164.

Yuck, don't like it.
2024-04-12 17:21:26 -07:00
Jeff McCune
fc275e4164 (#127) Angular - ng add ng-matero
Trying [ng-matero][1].  Seems to exceed the max prod budget of 1mb, but
worth trying anyway.

[1]: https://github.com/ng-matero/ng-matero
2024-04-12 17:18:33 -07:00
Jeff McCune
9fa466f7cf (#126) Build the front end app when building holos
Always build the front end app bundle when rebuilding the holos cli so
we're sure things are up to date.
2024-04-12 17:04:41 -07:00
Jeff McCune
efd6f256a5 (#126) Connect generated bindings for the frontend 2024-04-12 16:57:30 -07:00
Jeff McCune
f7f9d6b5f0 (#126) Angular Material - ng add @angular/material 2024-04-12 16:57:15 -07:00
Jeff McCune
0526062ab2 (#126) Configure Angular to align with frontend.go
Angular must build output into a path compatible with the Go
http.FileServer.  We cannot easily graft an fs.FS onto a sub-path, so we
need the `./ui/` path in the output.  This requires special
configuration from the Angular default application builder behavior.
2024-04-12 16:57:15 -07:00
Jeff McCune
a1ededa722 (#126) http.FileServer serves /ui instead of /app
This fixes Angular not being served up correctly.

Note, special configuration in Angular is necessary to get the build
output into the ui/ directory.  Refer to: [Output path configuration][1]
and [browser directory created in outputPath][2].

[1]: https://angular.io/guide/workspace-config#output-path-configuration
[2]: https://github.com/angular/angular-cli/issues/26304
2024-04-12 16:51:45 -07:00
Jeff McCune
9b09a02912 (#115) Angular new project with defaults
Setup angular with the defaults.  CSS, No SSR / Static Site Generation.

    npm install -g @angular/cli
    ng new holos

```
? Which stylesheet format would you like to use? CSS             [ https://developer.mozilla.org/docs/Web/CSS                     ]
? Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No
```

```
CREATE holos/README.md (1059 bytes)
CREATE holos/.editorconfig (274 bytes)
CREATE holos/.gitignore (548 bytes)
CREATE holos/angular.json (2587 bytes)
CREATE holos/package.json (1036 bytes)
CREATE holos/tsconfig.json (857 bytes)
CREATE holos/tsconfig.app.json (263 bytes)
CREATE holos/tsconfig.spec.json (273 bytes)
CREATE holos/.vscode/extensions.json (130 bytes)
CREATE holos/.vscode/launch.json (470 bytes)
CREATE holos/.vscode/tasks.json (938 bytes)
CREATE holos/src/main.ts (250 bytes)
CREATE holos/src/favicon.ico (15086 bytes)
CREATE holos/src/index.html (291 bytes)
CREATE holos/src/styles.css (80 bytes)
CREATE holos/src/app/app.component.css (0 bytes)
CREATE holos/src/app/app.component.html (19903 bytes)
CREATE holos/src/app/app.component.spec.ts (913 bytes)
CREATE holos/src/app/app.component.ts (301 bytes)
CREATE holos/src/app/app.config.ts (227 bytes)
CREATE holos/src/app/app.routes.ts (77 bytes)
CREATE holos/src/assets/.gitkeep (0 bytes)
✔ Packages installed successfully.
```
2024-04-12 15:07:38 -07:00
Jeff McCune
657a5e82a5 (#115) Remove Angular SSR
We don't want Angular Server Side Rendering, we want plain old client
side angular.
2024-04-12 14:57:39 -07:00
Jeff McCune
1eece02254 (#126) Angular Material UI
ng add @angular/material

```
❯ ng add @angular/material
Skipping installation: Package already installed
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Include the Angular animations module? Include and enable animations Yes
```
2024-04-12 14:16:45 -07:00
Jeff McCune
c866b47dcb (#126) Check for errors decoding claims
Return an empty claims struct when there's an error.
2024-04-12 14:16:44 -07:00
Jeff McCune
ff52ec750b (#126) Try to fix golangci-lint
It's doing way too much, might want to consider something else.

Getting these errors:

```
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.dockerignore: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.envrc: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.gitattributes: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/CODEOWNERS: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/buf-logo.svg: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/dependabot.yml: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/workflows/add-to-project.yaml: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/workflows/back-to-development.yaml: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/workflows/buf-binary-size.yaml: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/workflows/buf-shadow-sync.yaml: Cannot open: File exists
/usr/bin/tar: ../../../go/pkg/mod/github.com/bufbuild/buf@v1.30.1/.github/workflows/buf.yaml: Cannot open: File exists
```
2024-04-12 14:01:16 -07:00
Jeff McCune
4184619afc (#126) Refactor pkg to internal
pkg folder is not needed.  Move everything internal for now.
2024-04-12 13:56:16 -07:00
Jeff McCune
954dbd1ec8 (#126) Refactor id token acquisition to token package
And add a logout command that deletes the token cache.

The token package is intended for subcommands that need to make API
calls to the holos api server, getting a token should be a simple matter
of calling the token.Get() method, which takes minimal dependencies.
2024-04-12 13:15:03 -07:00
Jeff McCune
30b70e76aa (#126) Add login command
This copies the login command from the previous holos cli.  Wire
dependency injection and all the rest of the unnecessary stuff from
kubelogin are removed, streamlined down into a single function that
takes a few oidc related parameters.

This will need to be extracted out into an infrastructure service so
multiple other command line tools can easily re-use it and get the ID
token into the x-oidc-id-token header.
2024-04-12 12:13:33 -07:00
Jeff McCune
ec6d112711 (#126) Remove hydra and kratos databases
No longer needed for dev.
2024-04-12 10:24:26 -07:00
Jeff McCune
e796c6a763 (#126) Default to DATABASE_URL env var 2024-04-12 10:20:13 -07:00
Jeff McCune
be32201294 (#126) Basic User and Organization Ent models
Get rid of the previous UserIdentity model, this is no longer part of
the core domain and instead handled within the context of ZITADEL.
2024-04-12 09:59:40 -07:00
Jeff McCune
5ebc54b5b7 (#124) Go Tools 2024-04-12 09:14:13 -07:00
Jeff McCune
2954a57872 (#120) Fix NATS target namespace
The upstream nats charts don't specify namespaces for each attribute.
This works with helm update, but not helm template which holos uses to
render the yaml.

The missing namespace causes flux to fail.

This patch uses the flux kustomization to add the target namespace to
all resources.
2024-04-10 21:54:58 -07:00
Jeff McCune
df705bd79f (#121) Fix Multiple Charts cause holos render to fail
When rendering a holos component which contains more than one helm chart, rendering fails.  It should succeed.

```
holos render --cluster-name=k2 /home/jeff/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/holos/... --log-level debug
```

```
9:03PM ERR could not execute version=0.64.2 err="could not rename: rename /home/jeff/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/holos/nats/envs/vendor553679311 /home/jeff/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/holos/nats/envs/vendor: file exists" loc=helm.go:145
```

This patch fixes the problem by moving each child item of the temporary
directory charts are installed into.  This avoids the problem of moving
the parent when the parent target already exists.
2024-04-10 21:27:39 -07:00
Jeff McCune
4e8ce3585d (#115) Minor clean up of cue code 2024-04-10 21:21:16 -07:00
632 changed files with 120375 additions and 7152 deletions

View File

@@ -30,14 +30,15 @@ jobs:
with:
go-version: stable
- name: Install tools
run: sudo apt update && sudo apt -qq -y install curl zip unzip tar bzip2 make
- name: Install Packages
run: sudo apt update && sudo apt -qq -y install git curl zip unzip tar bzip2 make
- name: Install Deps
- name: Install Tools
run: |
make go-deps
set -x
make tools
make buf
go generate ./...
make frontend-deps
make frontend
go mod tidy
@@ -45,3 +46,4 @@ jobs:
uses: golangci/golangci-lint-action@v4
with:
version: latest
skip-pkg-cache: true

View File

@@ -36,11 +36,12 @@ jobs:
# Necessary to run these outside of goreleaser, otherwise
# /home/runner/_work/holos/holos/internal/frontend/node_modules/.bin/protoc-gen-connect-query is not in PATH
- name: Install Deps
- name: Install Tools
run: |
make go-deps
set -x
make tools
make buf
go generate ./...
make frontend-deps
make frontend
go mod tidy
@@ -53,6 +54,9 @@ jobs:
- name: List keys
run: gpg -K
- name: Git diff
run: git diff
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:

View File

@@ -28,8 +28,8 @@ jobs:
with:
go-version: stable
- name: Install tools
run: sudo apt update && sudo apt -qq -y install curl zip unzip tar bzip2 make
- name: Install Packages
run: sudo apt update && sudo apt -qq -y install git curl zip unzip tar bzip2 make
- name: Set up Helm
uses: azure/setup-helm@v4
@@ -37,11 +37,12 @@ jobs:
- name: Set up Kubectl
uses: azure/setup-kubectl@v3
- name: Install Deps
- name: Install Tools
run: |
make go-deps
set -x
make tools
make buf
go generate ./...
make frontend-deps
make frontend
go mod tidy

3
.gitignore vendored
View File

@@ -1,4 +1,4 @@
bin/
/bin/
vendor/
.idea/
coverage.out
@@ -6,3 +6,4 @@ coverage.out
*.hold/
/deploy/
.vscode/
tmp/

View File

@@ -4,7 +4,7 @@ PROJ=holos
ORG_PATH=github.com/holos-run
REPO_PATH=$(ORG_PATH)/$(PROJ)
VERSION := $(shell cat pkg/version/embedded/major pkg/version/embedded/minor pkg/version/embedded/patch | xargs printf "%s.%s.%s")
VERSION := $(shell cat version/embedded/major version/embedded/minor version/embedded/patch | xargs printf "%s.%s.%s")
BIN_NAME := holos
DOCKER_REPO=quay.io/openinfrastructure/holos
@@ -16,10 +16,12 @@ $( shell mkdir -p bin)
export PATH := $(PWD)/internal/frontend/holos/node_modules/.bin:$(PATH)
GIT_COMMIT=$(shell git rev-parse HEAD)
GIT_SUFFIX=$(shell test -n "`git status --porcelain`" && echo "-dirty" || echo "")
GIT_DETAIL=$(shell git describe --tags HEAD)
GIT_TREE_STATE=$(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")
BUILD_DATE=$(shell date -Iseconds)
LD_FLAGS="-w -X ${ORG_PATH}/${PROJ}/pkg/version.GitCommit=${GIT_COMMIT} -X ${ORG_PATH}/${PROJ}/pkg/version.GitTreeState=${GIT_TREE_STATE} -X ${ORG_PATH}/${PROJ}/pkg/version.BuildDate=${BUILD_DATE}"
LD_FLAGS="-w -X ${ORG_PATH}/${PROJ}/version.GitDescribe=${GIT_DETAIL}${GIT_SUFFIX} -X ${ORG_PATH}/${PROJ}/version.GitCommit=${GIT_COMMIT} -X ${ORG_PATH}/${PROJ}/version.GitTreeState=${GIT_TREE_STATE} -X ${ORG_PATH}/${PROJ}/version.BuildDate=${BUILD_DATE}"
.PHONY: default
default: test
@@ -61,14 +63,26 @@ vet: ## Vet Go code.
.PHONY: gencue
gencue: ## Generate CUE definitions
cd docs/examples && cue get go github.com/holos-run/holos/api/...
cd internal/generate/platforms && cue get go github.com/holos-run/holos/api/v1alpha1/...
.PHONY: rmgen
rmgen: ## Remove generated code
git rm -rf service/gen/ internal/frontend/holos/src/app/gen/ || true
rm -rf service/gen/ internal/frontend/holos/src/app/gen/
git rm -rf internal/ent/
rm -rf internal/ent/
git restore --staged internal/ent/generate.go internal/ent/schema/
git restore internal/ent/generate.go internal/ent/schema/
.PHONY: regenerate
regenerate: generate ## Re-generate code (delete and re-create)
.PHONY: generate
generate: ## Generate code.
generate: buf gencue ## Generate code.
go generate ./...
.PHONY: build
build: generate ## Build holos executable.
build: generate frontend ## Build holos executable.
@echo "building ${BIN_NAME} ${VERSION}"
@echo "GOPATH=${GOPATH}"
go build -trimpath -o bin/$(BIN_NAME) -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/$(BIN_NAME)
@@ -87,6 +101,8 @@ test: ## Run tests.
.PHONY: lint
lint: ## Run linters.
buf lint
cd internal/frontend/holos && ng lint
golangci-lint run
.PHONY: coverage
@@ -99,15 +115,18 @@ snapshot: ## Go release snapshot
.PHONY: buf
buf: ## buf generate
cd service && buf mod update
cd service && buf dep update
buf generate
.PHONY: tools
tools: go-deps frontend-deps ## install tool dependencies
.PHONY: go-deps
go-deps: ## install go executables
go install github.com/bufbuild/buf/cmd/buf@v1
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@v1
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1
go-deps: ## tool versions pinned in tools.go
go install github.com/bufbuild/buf/cmd/buf
go install github.com/fullstorydev/grpcurl/cmd/grpcurl
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install connectrpc.com/connect/cmd/protoc-gen-connect-go
go install honnef.co/go/tools/cmd/staticcheck@latest
# curl https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash
@@ -123,8 +142,8 @@ frontend-deps: ## Setup npm and vite
.PHONY: frontend
frontend: buf
cd internal/frontend/holos && rm -rf dist
mkdir -p internal/frontend/holos/dist
cd internal/frontend/holos/dist && rm -rf app
cd internal/frontend/holos && ng build
touch internal/frontend/frontend.go

View File

@@ -99,6 +99,7 @@ docker_build_with_restart(
'--listen-port={}'.format(listen_port),
'--oidc-issuer=https://login.ois.run',
'--oidc-audience=262096764402729854@holos_platform',
'--log-level=debug',
'--metrics-port={}'.format(metrics_port),
],
dockerfile='./hack/tilt/Dockerfile',
@@ -190,7 +191,7 @@ k8s_resource(
],
resource_deps=[compile_id],
links=[
link('https://{}.holos.dev.k2.ois.run/app/'.format(developer), "Holos Web UI")
link('https://{}.app.dev.k2.holos.run/ui/'.format(developer), "Holos Web UI")
],
)
@@ -200,11 +201,6 @@ k8s_resource(
new_name=auth_id,
objects=[
'{}:virtualservice'.format(holos_server),
'{}-allow-groups:authorizationpolicy'.format(holos_server),
'{}-allow-nothing:authorizationpolicy'.format(holos_server),
'{}-allow-well-known-paths:authorizationpolicy'.format(holos_server),
'{}-auth:authorizationpolicy'.format(holos_server),
'{}:requestauthentication'.format(holos_server),
],
)

View File

@@ -16,6 +16,10 @@ type BuildPlan struct {
type BuildPlanSpec struct {
Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"`
Components BuildPlanComponents `json:"components,omitempty" yaml:"components,omitempty"`
// DeployFiles keys represent file paths relative to the cluster deploy
// directory. Map values represent the string encoded file contents. Used to
// write the argocd Application, but may be used to render any file from CUE.
DeployFiles FileContentMap `json:"deployFiles,omitempty" yaml:"deployFiles,omitempty"`
}
type BuildPlanComponents struct {
@@ -38,3 +42,14 @@ func (bp *BuildPlan) Validate() error {
}
return nil
}
func (bp *BuildPlan) ResultCapacity() (count int) {
if bp == nil {
return 0
}
count = len(bp.Spec.Components.HelmChartList) +
len(bp.Spec.Components.KubernetesObjectsList) +
len(bp.Spec.Components.KustomizeBuildList) +
len(bp.Spec.Components.Resources)
return count
}

View File

@@ -20,3 +20,11 @@ type HolosComponent struct {
func (hc *HolosComponent) NewResult() *Result {
return &Result{HolosComponent: *hc}
}
func (hc *HolosComponent) GetAPIVersion() string {
return hc.APIVersion
}
func (hc *HolosComponent) GetKind() string {
return hc.Kind
}

13
api/v1alpha1/form.go Normal file
View File

@@ -0,0 +1,13 @@
package v1alpha1
import object "github.com/holos-run/holos/service/gen/holos/object/v1alpha1"
// Form represents a collection of Formly json powered form.
type Form struct {
TypeMeta `json:",inline" yaml:",inline"`
Spec FormSpec `json:"spec" yaml:"spec"`
}
type FormSpec struct {
Form object.Form `json:"form" yaml:"form"`
}

View File

@@ -8,9 +8,9 @@ import (
"strings"
"github.com/holos-run/holos"
"github.com/holos-run/holos/pkg/errors"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/util"
)
// A HelmChart represents a helm command to provide chart values in order to render kubernetes api objects.
@@ -141,9 +141,25 @@ func cacheChart(ctx context.Context, path holos.InstancePath, chartDir string, c
log.Debug("helm pull", "stdout", helmOut.Stdout, "stderr", helmOut.Stderr)
cachePath := filepath.Join(string(path), chartDir)
if err := os.Rename(cacheTemp, cachePath); err != nil {
return errors.Wrap(fmt.Errorf("could not rename: %w", err))
if err := os.MkdirAll(cachePath, 0777); err != nil {
return errors.Wrap(fmt.Errorf("could not mkdir: %w", err))
}
items, err := os.ReadDir(cacheTemp)
if err != nil {
return errors.Wrap(fmt.Errorf("could not read directory: %w", err))
}
for _, item := range items {
src := filepath.Join(cacheTemp, item.Name())
dst := filepath.Join(cachePath, item.Name())
log.DebugContext(ctx, "rename", "src", src, "dst", dst)
if err := os.Rename(src, dst); err != nil {
return errors.Wrap(fmt.Errorf("could not rename: %w", err))
}
}
log.InfoContext(ctx, "cached", "chart", chart.Name, "version", chart.Version, "path", cachePath)
return nil

View File

@@ -4,9 +4,9 @@ import (
"context"
"github.com/holos-run/holos"
"github.com/holos-run/holos/pkg/errors"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/util"
)
const KustomizeBuildKind = "KustomizeBuild"

32
api/v1alpha1/platform.go Normal file
View File

@@ -0,0 +1,32 @@
package v1alpha1
import "google.golang.org/protobuf/types/known/structpb"
// Platform represents a platform to manage. A Platform resource informs holos
// which components to build. The platform resource also acts as a container
// for the platform model form values provided by the PlatformService. The
// primary use case is to collect the cluster names, cluster types, platform
// model, and holos components to build into one resource.
type Platform struct {
TypeMeta `json:",inline" yaml:",inline"`
Metadata ObjectMeta `json:"metadata" yaml:"metadata"`
Spec PlatformSpec `json:"spec" yaml:"spec"`
}
// PlatformSpec represents the platform build plan specification.
type PlatformSpec struct {
// Model represents the platform model holos gets from from the
// holos.platform.v1alpha1.PlatformService.GetPlatform method and provides to
// CUE using a tag.
Model structpb.Struct `json:"model" yaml:"model"`
Components []PlatformSpecComponent `json:"components" yaml:"components"`
}
// PlatformSpecComponent represents a component to build or render with flags to
// pass, for example the cluster name.
type PlatformSpecComponent struct {
// Path is the path of the component relative to the platform root.
Path string `json:"path" yaml:"path"`
// Cluster is the cluster name to use when building the component.
Cluster string `json:"cluster" yaml:"cluster"`
}

View File

@@ -7,9 +7,9 @@ import (
"path/filepath"
"slices"
"github.com/holos-run/holos/pkg/errors"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/util"
)
// Result is the build result for display or writing. Holos components Render the Result as a data pipeline.
@@ -17,6 +17,18 @@ type Result struct {
HolosComponent
// accumulatedOutput accumulates rendered api objects.
accumulatedOutput string
// DeployFiles keys represent file paths relative to the cluster deploy
// directory. Map values represent the string encoded file contents. Used to
// write the argocd Application, but may be used to render any file from CUE.
DeployFiles FileContentMap `json:"deployFiles,omitempty" yaml:"deployFiles,omitempty"`
}
// Continue returns true if Skip is true indicating the result is to be skipped over.
func (r *Result) Continue() bool {
if r == nil {
return false
}
return r.Skip
}
func (r *Result) Name() string {
@@ -32,6 +44,11 @@ func (r *Result) KustomizationFilename(writeTo string, cluster string) string {
return filepath.Join(writeTo, "clusters", cluster, "holos", "components", r.Metadata.Name+"-kustomization.gen.yaml")
}
// KustomizationContent returns the kustomization file contents to write.
func (r *Result) KustomizationContent() string {
return r.KsContent
}
// AccumulatedOutput returns the accumulated rendered output.
func (r *Result) AccumulatedOutput() string {
return r.accumulatedOutput
@@ -120,6 +137,21 @@ func (r *Result) kustomize(ctx context.Context) error {
return nil
}
func (r *Result) WriteDeployFiles(ctx context.Context, path string) error {
log := logger.FromContext(ctx)
if len(r.DeployFiles) == 0 {
return nil
}
for k, content := range r.DeployFiles {
path := filepath.Join(path, k)
if err := r.Save(ctx, path, content); err != nil {
return errors.Wrap(err)
}
log.InfoContext(ctx, "wrote deploy file", "path", path, "bytes", len(content))
}
return nil
}
// Save writes the content to the filesystem for git ops.
func (r *Result) Save(ctx context.Context, path string, content string) error {
log := logger.FromContext(ctx)
@@ -128,7 +160,7 @@ func (r *Result) Save(ctx context.Context, path string, content string) error {
log.WarnContext(ctx, "could not mkdir", "path", dir, "err", err)
return errors.Wrap(err)
}
// Write the kube api objects
// Write the file content
if err := os.WriteFile(path, []byte(content), os.FileMode(0644)); err != nil {
log.WarnContext(ctx, "could not write", "path", path, "err", err)
return errors.Wrap(err)

View File

@@ -8,3 +8,13 @@ type TypeMeta struct {
func (tm *TypeMeta) GetKind() string {
return tm.Kind
}
func (tm *TypeMeta) GetAPIVersion() string {
return tm.Kind
}
// Discriminator is an interface to discriminate the kind api object.
type Discriminator interface {
GetKind() string
GetAPIVersion() string
}

View File

@@ -11,14 +11,10 @@ plugins:
out: service/gen
opt: paths=source_relative
- plugin: es
out: internal/frontend/holos/gen
out: internal/frontend/holos/src/app/gen
opt:
- target=ts
- plugin: connect-es
out: internal/frontend/holos/gen
opt:
- target=ts
- plugin: connect-query
out: internal/frontend/holos/gen
out: internal/frontend/holos/src/app/gen
opt:
- target=ts

View File

@@ -1,8 +1,9 @@
package main
import (
"github.com/holos-run/holos/pkg/cli"
"os"
"github.com/holos-run/holos/internal/cli"
)
func main() {

View File

@@ -1,10 +1,11 @@
package main
import (
"github.com/holos-run/holos/pkg/cli"
"github.com/rogpeppe/go-internal/testscript"
"os"
"testing"
"github.com/holos-run/holos/internal/cli"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {

View File

@@ -2,6 +2,8 @@
exec holos build ./foo/... --log-level debug
stdout '^bf2bc7f9-9ba0-4f9e-9bd2-9a205627eb0b$'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- foo/constraints.cue --
@@ -20,6 +22,7 @@ spec: components: KubernetesObjectsList: [
package holos
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
#KubernetesObjects: {
apiVersion: "holos.run/v1alpha1"

View File

@@ -3,12 +3,15 @@
stderr 'apiObjectMap.foo.bar: cannot convert incomplete value'
stderr '/component.cue:\d+:\d+$'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
package holos
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
apiVersion: "holos.run/v1alpha1"
kind: "BuildPlan"

View File

@@ -3,6 +3,8 @@ exec holos build .
stdout '^kind: SecretStore$'
stdout '# Source: CUE apiObjects.SecretStore.default'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
@@ -13,6 +15,7 @@ kind: "BuildPlan"
spec: components: KubernetesObjectsList: [{apiObjectMap: #APIObjects.apiObjectMap}]
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
#SecretStore: {
kind: string

View File

@@ -4,6 +4,8 @@ stdout '^kind: SecretStore$'
stdout '# Source: CUE apiObjects.SecretStore.default'
stderr 'skipping helm: no chart name specified'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
@@ -14,6 +16,7 @@ kind: "BuildPlan"
spec: components: HelmChartList: [{apiObjectMap: #APIObjects.apiObjectMap}]
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
#SecretStore: {
kind: string

View File

@@ -2,6 +2,8 @@
! exec holos build .
stderr 'apiObjects.secretstore.default.foo: field not allowed'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
@@ -10,6 +12,7 @@ package holos
apiVersion: "holos.run/v1alpha1"
kind: "KubernetesObjects"
cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
#SecretStore: {
metadata: name: string

View File

@@ -2,6 +2,8 @@
! exec holos build .
stderr 'Error: execution error at \(zitadel/templates/secret_zitadel-masterkey.yaml:2:4\): Either set .Values.zitadel.masterkey xor .Values.zitadel.masterkeySecretName'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- zitadel.cue --
@@ -12,6 +14,7 @@ kind: "BuildPlan"
spec: components: HelmChartList: [_HelmChart]
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
_HelmChart: {
apiVersion: "holos.run/v1alpha1"

View File

@@ -1,15 +1,18 @@
# Kustomize is a supported holos component kind
exec holos render --cluster-name=mycluster . --log-level=debug
exec holos render component --cluster-name=mycluster . --log-level=debug
# Want generated output
cmp want.yaml deploy/clusters/mycluster/components/kstest/kstest.gen.yaml
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
package holos
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
apiVersion: "holos.run/v1alpha1"
kind: "BuildPlan"

View File

@@ -3,11 +3,14 @@
! exec holos build .
stderr 'unknown field \\"TypoKubernetesObjectsList\\"'
-- platform.config.json --
{}
-- cue.mod --
package holos
-- component.cue --
package holos
_cluster: string @tag(cluster, string)
_platform_config: string @tag(platform_config, string)
apiVersion: "holos.run/v1alpha1"
kind: "BuildPlan"

View File

@@ -1,5 +1,3 @@
exec holos --version
# want version with no v on stdout
stdout -count=1 '^\d+\.\d+\.\d+$'
# want nothing on stderr
! stderr .

View File

@@ -0,0 +1,10 @@
{
"org_id": "018f36fb-e3f7-7f7f-a1c5-c85fb735d215",
"field_mask": {
"paths": [
"id",
"name",
"displayName"
]
}
}

View File

@@ -0,0 +1,8 @@
{
"update_mask": {
"paths": ["form"]
},
"update": {
"platform_id": "018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94"
}
}

View File

@@ -0,0 +1,11 @@
{
"update_mask": {
"paths": ["model","name","display_name"]
},
"update": {
"platform_id": "018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94",
"name": "bareplatform",
"display_name": "Bare Platform",
"model": {}
}
}

View File

@@ -0,0 +1,6 @@
{
"update": {
"platform_id": "018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94",
"model": {}
}
}

View File

@@ -2,7 +2,10 @@ package holos
import ap "security.istio.io/authorizationpolicy/v1"
// #AuthPolicyRules represents AuthorizationPolicy rules for hosts that need specialized treatment. Entries in this struct are exclused from the blank ingressauth AuthorizationPolicy governing the ingressgateway and included in a spcialized policy
// #AuthPolicyRules represents AuthorizationPolicy rules for hosts that need
// specialized treatment. Entries in this struct are excluded from
// AuthorizationPolicy/authproxy-custom in the istio-ingress namespace. Entries
// are added to their own AuthorizationPolicy.
#AuthPolicyRules: {
// AuthProxySpec represents the identity provider configuration
AuthProxySpec: #AuthProxySpec & #Platform.authproxy
@@ -14,6 +17,9 @@ import ap "security.istio.io/authorizationpolicy/v1"
name: Name
// slug is the resource name prefix
slug: string
// NoAuthorizationPolicy disables an AuthorizationPolicy for the host
NoAuthorizationPolicy: true | *false
// Refer to https://istio.io/latest/docs/reference/config/security/authorization-policy/#Rule
spec: ap.#AuthorizationPolicySpec & {
action: "CUSTOM"
@@ -25,11 +31,13 @@ import ap "security.istio.io/authorizationpolicy/v1"
objects: #APIObjects & {
for Host in hosts {
apiObjects: {
AuthorizationPolicy: "\(Host.slug)-custom": {
metadata: namespace: "istio-ingress"
metadata: name: "\(Host.slug)-custom"
spec: Host.spec
if Host.NoAuthorizationPolicy == false {
apiObjects: {
AuthorizationPolicy: "\(Host.slug)-custom": {
metadata: namespace: "istio-ingress"
metadata: name: "\(Host.slug)-custom"
spec: Host.spec
}
}
}
}

View File

@@ -4,3 +4,8 @@ package v1
apiVersion: "apps/v1"
kind: "Deployment"
}
#StatefulSet: {
apiVersion: "apps/v1"
kind: "StatefulSet"
}

View File

@@ -18,8 +18,10 @@ import "encoding/yaml"
Issuer?: [Name=_]: #Issuer & {metadata: name: Name}
Gateway?: [Name=_]: #Gateway & {metadata: name: Name}
ConfigMap?: [Name=_]: #ConfigMap & {metadata: name: Name}
ServiceAccount?: [Name=_]: #ServiceAccount & {metadata: name: Name}
Deployment?: [_]: #Deployment
StatefulSet?: [_]: #StatefulSet
RequestAuthentication?: [_]: #RequestAuthentication
AuthorizationPolicy?: [_]: #AuthorizationPolicy
}

View File

@@ -0,0 +1,26 @@
package holos
// NOTE: Beyond the base reference platform, services should typically be added to #OptionalServices instead of directly to a managed namespace.
// ManagedNamespace is a namespace to manage across all clusters in the holos platform.
#ManagedNamespace: {
namespace: {
metadata: {
name: string
labels: [string]: string
}
}
// clusterNames represents the set of clusters the namespace is managed on. Usually all clusters.
clusterNames: [...string]
for cluster in clusterNames {
clusters: (cluster): name: cluster
}
}
// #ManagedNamepsaces is the union of all namespaces across all cluster types and optional services.
// Holos adopts the namespace sameness position of SIG Multicluster, refer to https://github.com/kubernetes/community/blob/dd4c8b704ef1c9c3bfd928c6fa9234276d61ad18/sig-multicluster/namespace-sameness-position-statement.md
#ManagedNamespaces: {
[Name=_]: #ManagedNamespace & {
namespace: metadata: name: Name
}
}

View File

@@ -1,6 +1,8 @@
// Controls optional feature flags for services distributed across multiple holos components.
// For example, enable issuing certificates in the provisioner cluster when an optional service is
// enabled for a workload cluster.
// enabled for a workload cluster. Another example is NATS, which isn't necessary on all clusters,
// but is necessary on clusters with a project like holos which depends on NATS.
package holos
import "list"

View File

@@ -0,0 +1,3 @@
package holos
_platform_config: string @tag(platform_config, type=string)

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,45 @@
package holos
let Namespace = "jeff-holos"
let Broker = "choria-broker"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-platform-issuer": _
metadata: name: "\(Namespace)-\(Broker)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Broker
"app.kubernetes.io/name": Broker
}
let OBJECTS = #APIObjects & {
apiObjects: {
Certificate: "\(Broker)-tls": #Certificate & {
metadata: {
name: "\(Broker)-tls"
namespace: Namespace
labels: SelectorLabels
}
spec: {
commonName: "\(Broker).\(Namespace).svc.cluster.local"
dnsNames: [
Broker,
"\(Broker).\(Namespace).svc",
"\(Broker).\(Namespace).svc.cluster.local",
"*.\(Broker)",
"*.\(Broker).\(Namespace).svc",
"*.\(Broker).\(Namespace).svc.cluster.local",
]
issuerRef: kind: "ClusterIssuer"
issuerRef: name: "platform-issuer"
secretName: metadata.name
usages: ["signing", "key encipherment", "server auth", "client auth"]
}
}
}
}

View File

@@ -0,0 +1,45 @@
package holos
let Namespace = "jeff-holos"
let Provisioner = "choria-provisioner"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-platform-issuer": _
metadata: name: "\(Namespace)-\(Provisioner)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Provisioner
"app.kubernetes.io/name": Provisioner
}
let OBJECTS = #APIObjects & {
apiObjects: {
Certificate: "\(Provisioner)-tls": #Certificate & {
metadata: {
name: "\(Provisioner)-tls"
namespace: Namespace
labels: SelectorLabels
}
spec: {
commonName: "\(Provisioner).\(Namespace).svc.cluster.local"
dnsNames: [
Provisioner,
"\(Provisioner).\(Namespace).svc",
"\(Provisioner).\(Namespace).svc.cluster.local",
"*.\(Provisioner)",
"*.\(Provisioner).\(Namespace).svc",
"*.\(Provisioner).\(Namespace).svc.cluster.local",
]
issuerRef: kind: "ClusterIssuer"
issuerRef: name: "platform-issuer"
secretName: metadata.name
usages: ["signing", "key encipherment", "server auth", "client auth"]
}
}
}
}

View File

@@ -0,0 +1,177 @@
package holos
let Namespace = "jeff-holos"
let Broker = "choria-broker"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-secrets-stores": _
metadata: name: "\(Namespace)-\(Broker)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/part-of": "choria"
"app.kubernetes.io/name": Broker
}
let Metadata = {
name: Broker
namespace: Namespace
labels: SelectorLabels
}
let OBJECTS = #APIObjects & {
apiObjects: {
ExternalSecret: "\(Broker)-tls": #ExternalSecret & {
metadata: name: "\(Broker)-tls"
metadata: namespace: Namespace
}
ExternalSecret: "\(Broker)": #ExternalSecret & {
metadata: name: Broker
metadata: namespace: Namespace
}
StatefulSet: "\(Broker)": {
metadata: Metadata
spec: {
selector: matchLabels: SelectorLabels
serviceName: Broker
template: metadata: labels: SelectorLabels
template: spec: {
containers: [
{
name: Broker
command: ["choria", "broker", "run", "--config", "/etc/choria/broker.conf"]
image: "registry.choria.io/choria/choria:0.28.0"
imagePullPolicy: "IfNotPresent"
ports: [
{
containerPort: 4222
name: "tcp-nats"
protocol: "TCP"
},
{
containerPort: 4333
name: "https-wss"
protocol: "TCP"
},
{
containerPort: 5222
name: "tcp-cluster"
protocol: "TCP"
},
{
containerPort: 8222
name: "http-stats"
protocol: "TCP"
},
]
livenessProbe: httpGet: {
path: "/healthz"
port: "http-stats"
}
readinessProbe: livenessProbe
resources: {}
securityContext: {}
volumeMounts: [
{
mountPath: "/etc/choria"
name: Broker
},
{
mountPath: "/etc/choria-tls"
name: "\(Broker)-tls"
},
]
},
]
securityContext: {}
serviceAccountName: Broker
volumes: [
{
name: Broker
secret: secretName: Broker
},
{
name: "\(Broker)-tls"
secret: secretName: "\(Broker)-tls"
},
]
}
}
}
ServiceAccount: "\(Broker)": #ServiceAccount & {
metadata: Metadata
}
Service: "\(Broker)": #Service & {
metadata: Metadata
spec: {
type: "ClusterIP"
clusterIP: "None"
selector: SelectorLabels
ports: [
{
name: "tcp-nats"
appProtocol: "tcp"
port: 4222
protocol: "TCP"
targetPort: "tcp-nats"
},
{
name: "tcp-cluster"
appProtocol: "tcp"
port: 5222
protocol: "TCP"
targetPort: "tcp-cluster"
},
{
name: "https-wss"
appProtocol: "https"
port: 443
protocol: "TCP"
targetPort: "https-wss"
},
]
}
}
DestinationRule: "\(Broker)-wss": #DestinationRule & {
_decriptions: "Configures Istio to connect to Choria using a cert issued by the Platform Issuer"
metadata: Metadata
spec: host: "\(Broker).\(Namespace).svc.cluster.local"
spec: trafficPolicy: tls: {
credentialName: "istio-ingress-mtls-cert"
mode: "MUTUAL"
// subjectAltNames is important, otherwise istio will fail to verify the
// choria broker upstream server. make sure this matches a value
// present in the choria broker's cert.
//
// kubectl get secret choria-broker-tls -o json | jq --exit-status
// '.data | map_values(@base64d)' | jq .\"tls.crt\" -r | openssl x509
// -text -noout -in -
subjectAltNames: [spec.host]
}
}
VirtualService: "\(Broker)-wss": #VirtualService & {
metadata: name: "\(Broker)-wss"
metadata: namespace: Namespace
spec: {
gateways: ["istio-ingress/default"]
hosts: ["jeff.provision.dev.\(#ClusterName).holos.run"]
http: [
{
route: [
{
destination: {
host: "\(Broker).\(Namespace).svc.cluster.local"
port: "number": 443
}
},
]
},
]
}
}
}
}

View File

@@ -0,0 +1,18 @@
FROM registry.choria.io/choria/provisioner:latest
RUN curl -Lo nsc.zip https://github.com/nats-io/nsc/releases/download/v2.8.6/nsc-linux-amd64.zip &&\
unzip nsc.zip && \
mv nsc /usr/local/bin/nsc && \
chmod 755 /usr/local/bin/nsc && \
rm -f nsc.zip
# TODO: Add jwt executable
# TODO: Add helper executable
USER choria
ENV USER=choria
ENTRYPOINT ["/usr/sbin/choria-provisioner"]
# These two files are expected to be in the provisioner secret.
CMD ["--config=/etc/provisioner/provisioner.yaml", "--choria-config=/etc/provisioner/choria.cfg"]

View File

@@ -0,0 +1,82 @@
package holos
let Namespace = "jeff-holos"
let Provisioner = "choria-provisioner"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-secrets-stores": _
metadata: name: "\(Namespace)-\(Provisioner)"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelectorLabels = {
"app.kubernetes.io/instance": Provisioner
"app.kubernetes.io/name": Provisioner
}
let Metadata = {
name: Provisioner
namespace: Namespace
labels: SelectorLabels
}
let OBJECTS = #APIObjects & {
apiObjects: {
ExternalSecret: "\(Provisioner)-tls": #ExternalSecret & {
metadata: name: "\(Provisioner)-tls"
metadata: namespace: Namespace
}
ExternalSecret: "\(Provisioner)": #ExternalSecret & {
metadata: name: Provisioner
metadata: namespace: Namespace
}
ServiceAccount: "\(Provisioner)": #ServiceAccount & {
metadata: Metadata
}
Deployment: "\(Provisioner)": {
metadata: Metadata
spec: {
selector: matchLabels: SelectorLabels
template: metadata: labels: SelectorLabels
template: spec: {
containers: [
{
name: Provisioner
command: ["bash", "/etc/provisioner/entrypoint"]
// skopeo inspect docker://registry.choria.io/choria/provisioner | jq .RepoTags
image: "registry.choria.io/choria/provisioner:0.15.1"
imagePullPolicy: "IfNotPresent"
resources: {}
securityContext: {}
volumeMounts: [
{
mountPath: "/etc/provisioner"
name: Provisioner
},
{
mountPath: "/etc/provisioner-tls"
name: "\(Provisioner)-tls"
},
]
},
]
securityContext: {}
serviceAccountName: Provisioner
volumes: [
{
name: Provisioner
secret: secretName: name
},
{
name: "\(Provisioner)-tls"
secret: secretName: name
},
]
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
# Machine Room Provisioner
This sub-tree contains Holos Components to manage a [Choria Provisioner][1]
system for the use case of provisioning `holos controller` instances. These
instances are implementations of Machine Room which are in turn implementations
of Choria Server, hence why we use Choria Provisioner.
[1]: https://choria-io.github.io/provisioner/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# curl -LO https://github.com/nats-io/nack/releases/latest/download/crds.yml
resources:
- crds.yml

View File

@@ -0,0 +1,8 @@
package holos
// NATS NetStream Controller (NACK)
spec: components: KustomizeBuildList: [
#KustomizeBuild & {
metadata: name: "prod-nack-crds"
},
]

View File

@@ -0,0 +1,62 @@
package holos
// for Project in _Projects {
// spec: components: resources: (#ProjectTemplate & {project: Project}).workload.resources
// }
let Namespace = "jeff-holos"
#Kustomization: spec: targetNamespace: Namespace
spec: components: HelmChartList: [
#HelmChart & {
metadata: name: "jeff-holos-nats"
namespace: Namespace
_dependsOn: "prod-secrets-stores": _
chart: {
name: "nats"
version: "1.1.10"
repository: NatsRepository
}
_values: #NatsValues & {
config: {
// https://github.com/nats-io/k8s/tree/main/helm/charts/nats#operator-mode-with-nats-resolver
resolver: enabled: true
resolver: merge: {
type: "full"
interval: "2m"
timeout: "1.9s"
}
merge: {
operator: "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJUSElBTDM2NUtOS0lVVVJDMzNLNFJGQkJVRlFBSTRLS0NQTDJGVDZYVjdNQVhWU1dFNElRIiwiaWF0IjoxNzEzMjIxMzE1LCJpc3MiOiJPREtQM0RZTzc3T1NBRU5IU0FFR0s3WUNFTFBYT1FFWUI3RVFSTVBLWlBNQUxINE5BRUVLSjZDRyIsIm5hbWUiOiJIb2xvcyIsInN1YiI6Ik9ES1AzRFlPNzdPU0FFTkhTQUVHSzdZQ0VMUFhPUUVZQjdFUVJNUEtaUE1BTEg0TkFFRUtKNkNHIiwibmF0cyI6eyJ0eXBlIjoib3BlcmF0b3IiLCJ2ZXJzaW9uIjoyfX0.dQURTb-zIQMc-OYd9328oY887AEnvog6gOXY1-VCsDG3L89nq5x_ks4ME7dJ4Pn-Pvm2eyBi1Jx6ubgkthHgCQ"
system_account: "ADIQCYK4K3OKTPODGCLI4PDQ6XBO52MISBPTAIDESEJMLZCMNULDKCCY"
resolver_preload: {
// NOTEL: Make sure you do not include the trailing , in the SYS_ACCOUNT_JWT
"ADIQCYK4K3OKTPODGCLI4PDQ6XBO52MISBPTAIDESEJMLZCMNULDKCCY": "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI2SEVMNlhKSUdWUElMNFBURVI1MkUzTkFITjZLWkVUUUdFTlFVS0JWRzNUWlNLRzVLT09RIiwiaWF0IjoxNzEzMjIxMzE1LCJpc3MiOiJPREtQM0RZTzc3T1NBRU5IU0FFR0s3WUNFTFBYT1FFWUI3RVFSTVBLWlBNQUxINE5BRUVLSjZDRyIsIm5hbWUiOiJTWVMiLCJzdWIiOiJBRElRQ1lLNEszT0tUUE9ER0NMSTRQRFE2WEJPNTJNSVNCUFRBSURFU0VKTUxaQ01OVUxES0NDWSIsIm5hdHMiOnsibGltaXRzIjp7InN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTEsImltcG9ydHMiOi0xLCJleHBvcnRzIjotMSwid2lsZGNhcmRzIjp0cnVlLCJjb25uIjotMSwibGVhZiI6LTF9LCJkZWZhdWx0X3Blcm1pc3Npb25zIjp7InB1YiI6e30sInN1YiI6e319LCJhdXRob3JpemF0aW9uIjp7fSwidHlwZSI6ImFjY291bnQiLCJ2ZXJzaW9uIjoyfX0.TiGIk8XON394D9SBEowGHY_nTeOyHiM-ihyw6HZs8AngOnYPFXH9OVjsaAf8Poa2k_V84VtH7yVNgNdjBgduDA"
}
}
cluster: enabled: true
jetstream: enabled: true
websocket: enabled: true
monitor: enabled: true
}
promExporter: enabled: true
promExporter: podMonitor: enabled: true
}
},
#HelmChart & {
metadata: name: "jeff-holos-nack"
namespace: Namespace
_dependsOn: "jeff-holos-nats": _
chart: {
name: "nack"
version: "0.25.2"
repository: NatsRepository
}
},
]
let NatsRepository = {
name: "nats"
url: "https://nats-io.github.io/k8s/helm/charts/"
}

View File

@@ -0,0 +1,722 @@
package holos
#NatsValues: {
//###############################################################################
// Global options
//###############################################################################
global: {
image: {
// global image pull policy to use for all container images in the chart
// can be overridden by individual image pullPolicy
pullPolicy: null
// global list of secret names to use as image pull secrets for all pod specs in the chart
// secrets must exist in the same namespace
// https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
pullSecretNames: []
// global registry to use for all container images in the chart
// can be overridden by individual image registry
registry: null
}
// global labels will be applied to all resources deployed by the chart
labels: {}
}
//###############################################################################
// Common options
//###############################################################################
// override name of the chart
nameOverride: null
// override full name of the chart+release
fullnameOverride: null
// override the namespace that resources are installed into
namespaceOverride: null
// reference a common CA Certificate or Bundle in all nats config `tls` blocks and nats-box contexts
// note: `tls.verify` still must be set in the appropriate nats config `tls` blocks to require mTLS
tlsCA: {
enabled: false
// set configMapName in order to mount an existing configMap to dir
configMapName: null
// set secretName in order to mount an existing secretName to dir
secretName: null
// directory to mount the configMap or secret to
dir: "/etc/nats-ca-cert"
// key in the configMap or secret that contains the CA Certificate or Bundle
key: "ca.crt"
}
//###############################################################################
// NATS Stateful Set and associated resources
//###############################################################################
//###########################################################
// NATS config
//###########################################################
config: {
cluster: {
enabled: true | *false
port: 6222
// must be 2 or higher when jetstream is enabled
replicas: 3
// apply to generated route URLs that connect to other pods in the StatefulSet
routeURLs: {
// if both user and password are set, they will be added to route URLs
// and the cluster authorization block
user: null
password: null
// set to true to use FQDN in route URLs
useFQDN: false
k8sClusterDomain: "cluster.local"
}
tls: {
enabled: true | *false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/cluster"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
// merge or patch the cluster config
// https://docs.nats.io/running-a-nats-service/configuration/clustering/cluster_config
merge: {}
patch: []
}
jetstream: {
enabled: true | *false
fileStore: {
enabled: true
dir: "/data"
//###########################################################
// stateful set -> volume claim templates -> jetstream pvc
//###########################################################
pvc: {
enabled: true
size: "10Gi"
storageClassName: null
// merge or patch the jetstream pvc
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-js"
name: null
}
// defaults to the PVC size
maxSize: null
}
memoryStore: {
enabled: false
// ensure that container has a sufficient memory limit greater than maxSize
maxSize: "1Gi"
}
// merge or patch the jetstream config
// https://docs.nats.io/running-a-nats-service/configuration#jetstream
merge: {}
patch: []
}
nats: {
port: 4222
tls: {
enabled: false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/nats"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
}
leafnodes: {
enabled: false
port: 7422
tls: {
enabled: false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/leafnodes"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
// merge or patch the leafnodes config
// https://docs.nats.io/running-a-nats-service/configuration/leafnodes/leafnode_conf
merge: {}
patch: []
}
websocket: {
enabled: true | *false
port: 8080
tls: {
enabled: false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/websocket"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
//###########################################################
// ingress
//###########################################################
// service must be enabled also
ingress: {
enabled: false
// must contain at least 1 host otherwise ingress will not be created
hosts: []
path: "/"
pathType: "Exact"
// sets to the ingress class name
className: null
// set to an existing secret name to enable TLS on the ingress; applies to all hosts
tlsSecretName: null
// merge or patch the ingress
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#ingress-v1-networking-k8s-io
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-ws"
name: null
}
// merge or patch the websocket config
// https://docs.nats.io/running-a-nats-service/configuration/websocket/websocket_conf
merge: {}
patch: []
}
mqtt: {
enabled: false
port: 1883
tls: {
enabled: false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/mqtt"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
// merge or patch the mqtt config
// https://docs.nats.io/running-a-nats-service/configuration/mqtt/mqtt_config
merge: {}
patch: []
}
gateway: {
enabled: false
port: 7222
tls: {
enabled: false
// set secretName in order to mount an existing secret to dir
secretName: null
dir: "/etc/nats-certs/gateway"
cert: "tls.crt"
key: "tls.key"
// merge or patch the tls config
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls
merge: {}
patch: []
}
// merge or patch the gateway config
// https://docs.nats.io/running-a-nats-service/configuration/gateways/gateway#gateway-configuration-block
merge: {}
patch: []
}
monitor: {
enabled: true
port: 8222
tls: {
// config.nats.tls must be enabled also
// when enabled, monitoring port will use HTTPS with the options from config.nats.tls
enabled: false
}
}
profiling: {
enabled: false
port: 65432
}
resolver: {
enabled: true | *false
dir: "/data/resolver"
//###########################################################
// stateful set -> volume claim templates -> resolver pvc
//###########################################################
pvc: {
enabled: true
size: "1Gi"
storageClassName: null
// merge or patch the pvc
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-resolver"
name: null
}
// merge or patch the resolver
// https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt/resolver
merge: {
type?: string
interval?: string
timeout?: string
}
patch: []
}
// adds a prefix to the server name, which defaults to the pod name
// helpful for ensuring server name is unique in a super cluster
serverNamePrefix: ""
// merge or patch the nats config
// https://docs.nats.io/running-a-nats-service/configuration
// following special rules apply
// 1. strings that start with << and end with >> will be unquoted
// use this for variables and numbers with units
// 2. keys ending in $include will be switched to include directives
// keys are sorted alphabetically, use prefix before $includes to control includes ordering
// paths should be relative to /etc/nats-config/nats.conf
// example:
//
// merge:
// $include: ./my-config.conf
// zzz$include: ./my-config-last.conf
// server_name: nats
// authorization:
// token: << $TOKEN >>
// jetstream:
// max_memory_store: << 1GB >>
//
// will yield the config:
// {
// include ./my-config.conf;
// "authorization": {
// "token": $TOKEN
// },
// "jetstream": {
// "max_memory_store": 1GB
// },
// "server_name": "nats",
// include ./my-config-last.conf;
// }
merge: {
operator?: string
system_account?: string
resolver_preload?: [string]: string
}
patch: []
}
//###########################################################
// stateful set -> pod template -> nats container
//###########################################################
container: {
image: {
repository: "nats"
tag: "2.10.12-alpine"
pullPolicy: null
registry: null
}
// container port options
// must be enabled in the config section also
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#containerport-v1-core
ports: {
nats: {}
leafnodes: {}
websocket: {}
mqtt: {}
cluster: {}
gateway: {}
monitor: {}
profiling: {}
}
// map with key as env var name, value can be string or map
// example:
//
// env:
// GOMEMLIMIT: 7GiB
// TOKEN:
// valueFrom:
// secretKeyRef:
// name: nats-auth
// key: token
env: {}
// merge or patch the container
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core
merge: {}
patch: []
}
//###########################################################
// stateful set -> pod template -> reloader container
//###########################################################
reloader: {
enabled: true
image: {
repository: "natsio/nats-server-config-reloader"
tag: "0.14.1"
pullPolicy: null
registry: null
}
// env var map, see nats.env for an example
env: {}
// all nats container volume mounts with the following prefixes
// will be mounted into the reloader container
natsVolumeMountPrefixes: ["/etc/"]
// merge or patch the container
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core
merge: {}
patch: []
}
//###########################################################
// stateful set -> pod template -> prom-exporter container
//###########################################################
// config.monitor must be enabled
promExporter: {
enabled: true | *false
image: {
repository: "natsio/prometheus-nats-exporter"
tag: "0.14.0"
pullPolicy: null
registry: null
}
port: 7777
// env var map, see nats.env for an example
env: {}
// merge or patch the container
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core
merge: {}
patch: []
//###########################################################
// prometheus pod monitor
//###########################################################
podMonitor: {
enabled: true | *false
// merge or patch the pod monitor
// https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}"
name: null
}
}
//###########################################################
// service
//###########################################################
service: {
enabled: true
// service port options
// additional boolean field enable to control whether port is exposed in the service
// must be enabled in the config section also
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceport-v1-core
ports: {
nats: enabled: true
leafnodes: enabled: true
websocket: enabled: true
mqtt: enabled: true
cluster: enabled: false
gateway: enabled: false
monitor: enabled: false
profiling: enabled: false
}
// merge or patch the service
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}"
name: null
}
//###########################################################
// other nats extension points
//###########################################################
// stateful set
statefulSet: {
// merge or patch the stateful set
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#statefulset-v1-apps
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}"
name: null
}
// stateful set -> pod template
podTemplate: {
// adds a hash of the ConfigMap as a pod annotation
// this will cause the StatefulSet to roll when the ConfigMap is updated
configChecksumAnnotation: true
// map of topologyKey: topologySpreadConstraint
// labelSelector will be added to match StatefulSet pods
//
// topologySpreadConstraints:
// kubernetes.io/hostname:
// maxSkew: 1
//
topologySpreadConstraints: {}
// merge or patch the pod template
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core
merge: {}
patch: []
}
// headless service
headlessService: {
// merge or patch the headless service
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-headless"
name: null
}
// config map
configMap: {
// merge or patch the config map
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#configmap-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-config"
name: null
}
// pod disruption budget
podDisruptionBudget: {
enabled: true
// merge or patch the pod disruption budget
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#poddisruptionbudget-v1-policy
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}"
name: null
}
// service account
serviceAccount: {
enabled: false
// merge or patch the service account
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceaccount-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}"
name: null
}
//###########################################################
// natsBox
//
// NATS Box Deployment and associated resources
//###########################################################
natsBox: {
enabled: true
//###########################################################
// NATS contexts
//###########################################################
contexts: {
default: {
creds: {
// set contents in order to create a secret with the creds file contents
contents: null
// set secretName in order to mount an existing secret to dir
secretName: null
// defaults to /etc/nats-creds/<context-name>
dir: null
key: "nats.creds"
}
nkey: {
// set contents in order to create a secret with the nkey file contents
contents: null
// set secretName in order to mount an existing secret to dir
secretName: null
// defaults to /etc/nats-nkeys/<context-name>
dir: null
key: "nats.nk"
}
// used to connect with client certificates
tls: {
// set secretName in order to mount an existing secret to dir
secretName: null
// defaults to /etc/nats-certs/<context-name>
dir: null
cert: "tls.crt"
key: "tls.key"
}
// merge or patch the context
// https://docs.nats.io/using-nats/nats-tools/nats_cli#nats-contexts
merge: {}
patch: []
}
}
// name of context to select by default
defaultContextName: "default"
//###########################################################
// deployment -> pod template -> nats-box container
//###########################################################
container: {
image: {
repository: "natsio/nats-box"
tag: "0.14.2"
pullPolicy: null
registry: null
}
// env var map, see nats.env for an example
env: {}
// merge or patch the container
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core
merge: {}
patch: []
}
//###########################################################
// other nats-box extension points
//###########################################################
// deployment
deployment: {
// merge or patch the deployment
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#deployment-v1-apps
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-box"
name: null
}
// deployment -> pod template
podTemplate: {
// merge or patch the pod template
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core
merge: {}
patch: []
}
// contexts secret
contextsSecret: {
// merge or patch the context secret
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-box-contexts"
name: null
}
// contents secret
contentsSecret: {
// merge or patch the contents secret
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-box-contents"
name: null
}
// service account
serviceAccount: {
enabled: false
// merge or patch the service account
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceaccount-v1-core
merge: {}
patch: []
// defaults to "{{ include "nats.fullname" $ }}-box"
name: null
}
}
//###############################################################################
// Extra user-defined resources
//###############################################################################
//
// add arbitrary user-generated resources
// example:
//
// config:
// websocket:
// enabled: true
// extraResources:
// - apiVersion: networking.istio.io/v1beta1
// kind: VirtualService
// metadata:
// name:
// $tplYaml: >
// {{ include "nats.fullname" $ | quote }}
// labels:
// $tplYaml: |
// {{ include "nats.labels" $ }}
// spec:
// hosts:
// - demo.nats.io
// gateways:
// - my-gateway
// http:
// - name: default
// match:
// - name: root
// uri:
// exact: /
// route:
// - destination:
// host:
// $tplYaml: >
// {{ .Values.service.name | quote }}
// port:
// number:
// $tplYaml: >
// {{ .Values.config.websocket.port }}
//
extraResources: []
}

View File

@@ -0,0 +1,81 @@
#! /bin/bash
#
# This script initializes authorization for a nats cluster. The process is:
#
# Locally:
# 1. Generate the nats operator jwt.
# 2. Generate a SYS account jwt issued by the operator.
# 3. Store both into vault
#
# When nats is deployed an ExternalSecret populates auth.conf which is included
# into nats.conf. This approach allows helm values to be used for most things
# except for secrets.
#
# Clean up by removing the nsc directory.
set -euo pipefail
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap finish EXIT
PARENT="$(cd "$(dirname $0)" && pwd)"
: "${OPERATOR_NAME:="Holos"}"
: "${OIX_NAMESPACE:=$(kubectl config view --minify --flatten -ojsonpath='{.contexts[0].context.namespace}')}"
nsc="${HOME}/.bin/nsc"
ROOT="${PARENT}/${OIX_NAMESPACE}/nsc"
export NKEYS_PATH="${ROOT}/nkeys"
export NSC_HOME="${ROOT}/accounts"
mkdir -p "$NKEYS_PATH"
mkdir -p "$NSC_HOME"
# Install nsc if not already installed
if ! [[ -x $nsc ]]; then
platform="$(kubectl version --output=json | jq .clientVersion.platform -r)"
platform="${platform//\//-}"
curl -fSLo "${tmpdir}/nsc.zip" "https://github.com/nats-io/nsc/releases/download/v2.8.6/nsc-${platform}.zip"
(cd "${tmpdir}" && unzip nsc.zip)
sudo install -o 0 -g 0 -m 0755 "${tmpdir}/nsc" $nsc
fi
echo "export NKEYS_PATH='${NKEYS_PATH}'" > "${ROOT}/nsc.env"
echo "export NSC_HOME='${NSC_HOME}'" >> "${ROOT}/nsc.env"
# use kubectl port-forward nats-headless 4222
echo "export NATS_URL='nats://localhost:4222'" >> "${ROOT}/nsc.env"
echo "export NATS_CREDS='${ROOT}/nkeys/creds/${OPERATOR_NAME}/SYS/sys.creds'" >> "${ROOT}/nsc.env"
echo "export NATS_CA='${ROOT}/ca.crt'" >> "${ROOT}/nsc.env"
echo "export NATS_CERT='${ROOT}/tls.crt'" >> "${ROOT}/nsc.env"
echo "export NATS_KEY='${ROOT}/tls.key'" >> "${ROOT}/nsc.env"
$nsc --data-dir="${ROOT}/stores" list operators
# Create operator
$nsc add operator --name "${OPERATOR_NAME}"
# Create system account
$nsc add account --name SYS
$nsc add user --name sys
# Create account for STAN purposes.
$nsc add account --name STAN
$nsc add user --name stan
# Generate an auth config compatible with the StatefulSet mounting the
# nats-jwt-pvc PersistentVolumeClaim at path /data/accounts
$nsc generate config --sys-account SYS --nats-resolver \
| sed "s,dir.*jwt',dir: '/data/accounts'" \
> "${ROOT}/auth.conf"
# Store the auth config in vault.
# vault kv put kv/${OIX_CLUSTER_NAME}/kube-namespace/holos-dev/nats-auth-config "auth.conf=@${tmpdir}/auth.conf"
# Store the SYS creds in vault for use by the nack controller.
# vault kv put kv/${OIX_CLUSTER_NAME}/kube-namespace/holos-dev/nats-sys-creds "sys.creds=@${OIX_CLUSTER_NAME}/nsc/nkeys/creds/${OPERATOR_NAME}/SYS/sys.creds"
echo "After deploying the nats component, use the get-cert command to fetch the client cert."
echo "Use kubectl port-forward svc/nats-headless 4222" >&2
echo "source ${ROOT}/nsc.env to make it all work." >&2

View File

@@ -0,0 +1,5 @@
# Holos
This subtree contains holos components for holos itself. We strive for minimal dependencies, so this is likely going to contain NATS and/or Postgres resources.
Components depend on the holos project and may iterate over the defined environments in the project stages.

View File

@@ -71,14 +71,15 @@ let IstioInject = [{op: "add", path: "/spec/template/metadata/labels/sidecar.ist
}
}
// Probably shouldn't use the authproxy struct and should instead define an identity provider struct.
let AuthProxySpec = #AuthProxySpec & #Platform.authproxy
let OAuthClient = #Platform.oauthClients.argocd.spec
let OIDCConfig = {
name: "Holos Platform"
issuer: AuthProxySpec.issuer
clientID: #Platform.argocd.clientID
requestedIDTokenClaims: groups: essential: true
requestedScopes: ["openid", "profile", "email", "groups", "urn:zitadel:iam:org:domain:primary:\(AuthProxySpec.orgDomain)"]
name: "Holos Platform"
issuer: OAuthClient.issuer
clientID: OAuthClient.clientID
requestedScopes: OAuthClient.scopesList
// Set redirect uri to https://argocd.example.com/pkce/verify
enablePKCEAuthentication: true
requestedIDTokenClaims: groups: essential: true
}

View File

@@ -21,12 +21,12 @@ spec: components: KubernetesObjectsList: [
// GatewayServers represents all hosts for all VirtualServices in the cluster attached to Gateway/default
// NOTE: This is a critical structure because the default Gateway should be used in most cases.
let GatewayServers = {
// Critical Feature: Map all Project hosts to the default Gateway.
for Project in _Projects {
for server in (#ProjectTemplate & {project: Project}).ClusterGatewayServers {
(server.port.name): server
}
(#ProjectTemplate & {project: Project}).ClusterDefaultGatewayServers
}
// TODO: Refactor to use FQDN as key
for k, svc in #OptionalServices {
if svc.enabled && list.Contains(svc.clusterNames, #ClusterName) {
for server in svc.servers {
@@ -35,6 +35,7 @@ let GatewayServers = {
}
}
// TODO: Remove? Why aren't these part of the platform project?
if #PlatformServers[#ClusterName] != _|_ {
for server in #PlatformServers[#ClusterName] {
(server.port.name): server
@@ -52,6 +53,11 @@ let OBJECTS = #APIObjects & {
spec: servers: [for x in GatewayServers {x}]
}
// Manage an ExternalSecret for each server defined in the default Gateway to sync the cert.
for Server in Gateway.default.spec.servers {
ExternalSecret: "\(Server.tls.credentialName)": metadata: namespace: "istio-ingress"
}
for k, svc in #OptionalServices {
if svc.enabled && list.Contains(svc.clusterNames, #ClusterName) {
for k, s in svc.servers {

View File

@@ -8,7 +8,7 @@ let ComponentName = "\(#InstancePrefix)-ingress"
spec: components: HelmChartList: [
#HelmChart & {
_dependsOn: "prod-secrets-namespaces": _
_dependsOn: "prod-secrets-stores": _
_dependsOn: "\(#InstancePrefix)-istio-base": _
_dependsOn: "\(#InstancePrefix)-istiod": _
@@ -76,6 +76,10 @@ let RedirectMetaName = {
let OBJECTS = #APIObjects & {
apiObjects: {
ExternalSecret: "istio-ingress-mtls-cert": #ExternalSecret & {
metadata: name: "istio-ingress-mtls-cert"
metadata: namespace: #TargetNamespace
}
Gateway: {
"\(RedirectMetaName.name)": #Gateway & {
metadata: RedirectMetaName

View File

@@ -18,6 +18,10 @@ _IngressAuthProxy: {
Domains: (#Platform.org.domain): _
Domains: "\(#ClusterName).\(#Platform.org.domain)": _
// TODO: This should be generated from ProjectHosts
Domains: "holos.run": _
Domains: "\(#ClusterName).holos.run": _
let Metadata = {
name: string
namespace: Namespace
@@ -85,7 +89,8 @@ _IngressAuthProxy: {
spec: {
securityContext: seccompProfile: type: "RuntimeDefault"
containers: [{
image: "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0"
// image: "quay.io/oauth3-proxy/oauth2-proxy:v7.6.0"
image: "quay.io/holos/oauth2-proxy:v7.6.0-1-g77a03ae2"
imagePullPolicy: "IfNotPresent"
name: "oauth2-proxy"
volumeMounts: [{
@@ -271,11 +276,14 @@ _IngressAuthProxy: {
rules: [
{
to: [{
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
operation: notHosts: [
// Never send requests for the login service through the authorizer, would block login.
AuthProxySpec.issuerHost,
"\(AuthProxySpec.issuerHost):*",
// Exclude hosts with specialized rules from the catch-all.
for x in _AuthPolicyRules.hosts {x.name},
for x in _AuthPolicyRules.hosts {"\(x.name):*"},
]
}]
when: [
@@ -298,7 +306,7 @@ _IngressAuthProxy: {
_AuthPolicyRules: #AuthPolicyRules & {
hosts: {
let Vault = "vault.core.ois.run"
(Vault): {
"\(Vault)": {
slug: "vault"
// Rules for when to route requests through the auth proxy
spec: rules: [
@@ -321,3 +329,20 @@ _AuthPolicyRules: #AuthPolicyRules & {
}
}
}
// Exclude project hosts from the auth proxy if configured to do so. The
// intended effect is to exclude the host from the blanket `authproxy-custom`
// AuthorizationPolicy rule _without_ adding a specialized AuthorizationPolicy
// for the same host. This has the effect of completely excluding the host from
// authorization policy.
for Project in _Projects {
let ProjectHosts = (#ProjectHosts & {project: Project}).Hosts
for FQDN, Host in ProjectHosts {
if Host.NoAuthorizationPolicy {
if Host.clusters[#ClusterName] != _|_ {
_AuthPolicyRules: hosts: "\(Host.fqdn)": NoAuthorizationPolicy: true
}
}
}
}

View File

@@ -4,7 +4,7 @@ package holos
let Namespace = "prod-platform"
// FYI: kube-prometheus-stack is a large umbrella chart what brings in other large charts like
// FYI: kube-prometheus-stack is a large umbrella chart that brings in other large charts like
// [grafana](https://github.com/grafana/helm-charts/tree/main/charts/grafana).
// This may make affect maintainability. Consider breaking the integration down into
// constituent charts represented as holos component instances.
@@ -77,7 +77,7 @@ spec: components: HelmChartList: [
token_url: OIDC.token_endpoint
api_url: OIDC.userinfo_endpoint
use_pkce: true
name_attribute_path: name
name_attribute_path: "name"
// TODO: Lift the admin, editor, and viewer group names up to the plaform config struct.
role_attribute_path: "contains(groups[*], 'prod-cluster-admin') && 'Admin' || contains(groups[*], 'prod-cluster-editor') && 'Editor' || 'Viewer'"
}

View File

@@ -4,15 +4,23 @@ package holos
let ZitadelProjectID = 257713952794870157
let AllClusters = {
// platform level services typically run in the core cluster pair.
core1: _
core2: _
// for development, probably wouldn't run these services in the workload clusters.
k1: _
k2: _
k3: _
k4: _
k5: _
}
_Projects: #Projects & {
// The platform project is required and where platform services reside. ArgoCD, Grafana, Prometheus, etc...
platform: {
resourceId: ZitadelProjectID
// platform level services typically run in the core cluster pair.
clusters: core1: _
clusters: core2: _
// for development, probably wouldn't run these services in the workload clusters.
clusters: k2: _
clusters: AllClusters
// Services hosted in the platform project
hosts: argocd: _
hosts: grafana: _
@@ -21,8 +29,9 @@ _Projects: #Projects & {
holos: {
resourceId: ZitadelProjectID
clusters: k1: _
clusters: k2: _
domain: "holos.run"
clusters: AllClusters
environments: {
prod: stage: "prod"
dev: stage: "dev"
@@ -30,6 +39,13 @@ _Projects: #Projects & {
gary: stage: dev.stage
nate: stage: dev.stage
}
// app is the holos web app and grpc api.
hosts: app: _
// provision is the choria broker provisioning system.
hosts: provision: NoAuthorizationPolicy: true
// nats is the nats service holos controller machine room agents connect after provisioning.
hosts: nats: NoAuthorizationPolicy: true
}
iam: {

View File

@@ -0,0 +1,40 @@
package holos
// Certificate used by the ingress to connect to services using a platform
// issued certificate but which are not using istio sidecar injection.
// Examples are keycloak, vault, nats, choria, etc...
let Namespace = "istio-ingress"
let CertName = "istio-ingress-mtls-cert"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
_dependsOn: "prod-platform-issuer": _
metadata: name: CertName
apiObjectMap: OBJECTS.apiObjectMap
},
]
let OBJECTS = #APIObjects & {
apiObjects: {
Certificate: "\(CertName)": #Certificate & {
metadata: {
name: CertName
namespace: Namespace
}
spec: {
secretName: metadata.name
issuerRef: kind: "ClusterIssuer"
issuerRef: name: "platform-issuer"
commonName: "istio-ingress"
dnsNames: [
"istio-ingress",
"istio-ingress.\(Namespace)",
"istio-ingress.\(Namespace).svc",
"istio-ingress.\(Namespace).svc.cluster.local",
]
}
}
}
}

View File

@@ -0,0 +1,53 @@
package holos
// Refer to https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers
let Namespace = "cert-manager"
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
metadata: name: "prod-platform-issuer"
_dependsOn: "prod-mesh-certmanager": _
apiObjectMap: OBJECTS.apiObjectMap
},
]
let SelfSigned = "platform-selfsigned"
let PlatformIssuer = "platform-issuer"
let OBJECTS = #APIObjects & {
apiObjects: {
ClusterIssuer: {
"\(SelfSigned)": #ClusterIssuer & {
metadata: name: SelfSigned
spec: selfSigned: {}
}
}
Certificate: {
"\(PlatformIssuer)": #Certificate & {
metadata: name: PlatformIssuer
metadata: namespace: Namespace
spec: {
duration: "999999h"
isCA: true
commonName: PlatformIssuer
secretName: PlatformIssuer
privateKey: algorithm: "ECDSA"
privateKey: size: 256
issuerRef: {
name: SelfSigned
kind: "ClusterIssuer"
group: "cert-manager.io"
}
}
}
}
ClusterIssuer: {
"\(PlatformIssuer)": #ClusterIssuer & {
metadata: name: PlatformIssuer
spec: ca: secretName: PlatformIssuer
}
}
}
}

View File

@@ -0,0 +1,5 @@
# Platform Issuer
The platform issuer is a self signed root certificate authority which acts as a private pki for the platform. Used to issue certificates for use internally within the platform in a way that supports multi-cluster communication.
Refer to [Bootstrapping CA Issuers](https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers)

View File

@@ -1,5 +1,11 @@
package holos
for Project in _Projects {
spec: components: resources: (#ProjectTemplate & {project: Project}).provisioner.resources
// Debugging variable to enable inspecting the project host data:
// cue eval --out json -t cluster=provisioner ./platforms/reference/clusters/provisioner/projects/... -e _ProjectHosts.holos > hosts.json
let ProjectData = (#ProjectTemplate & {project: Project})
_ProjectHosts: "\(Project.name)": ProjectData.ProjectHosts
spec: components: resources: ProjectData.provisioner.resources
}

View File

@@ -0,0 +1,73 @@
package holos
let Namespace = "dev-holos"
let Holos = "holos"
// spec represents the output provided to holos
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
metadata: name: "dev-holos-app"
apiObjectMap: OBJECTS.apiObjectMap
},
]
// OBJECTS represents the kubernetes api objects to manage.
let OBJECTS = #APIObjects & {
apiObjects: Deployment: holos: {
metadata: {
name: Holos
namespace: Namespace
labels: app: Holos
}
spec: {
selector: matchLabels: app: Holos
template: metadata: labels: {
app: Holos
"sidecar.istio.io/inject": "true"
}
strategy: rollingUpdate: maxSurge: 1
strategy: rollingUpdate: maxUnavailable: 0
template: {
spec: {
serviceAccountName: Holos
securityContext: seccompProfile: type: "RuntimeDefault"
containers: [
{
name: Holos
image: "271053619184.dkr.ecr.us-east-2.amazonaws.com/holos-run/holos-server/holos:v0.79.0"
imagePullPolicy: "Always"
env: [
{
name: "TZ"
value: "America/Los_Angeles"
},
{
name: "DATABASE_URL"
valueFrom: secretKeyRef: {
key: "uri"
name: "holos-pguser-holos"
}
},
]
ports: [
{
containerPort: 3000
name: "http"
protocol: "TCP"
},
]
securityContext: capabilities: drop: ["ALL"]
securityContext: allowPrivilegeEscalation: false
securityContext: runAsNonRoot: true
resources: limits: {
cpu: "0.25"
memory: "256Mi"
}
resources: requests: resources.limits
},
]
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
package holos
let Namespace = "dev-holos"
let Holos = "holos"
// spec represents the output provided to holos
spec: components: KubernetesObjectsList: [
#KubernetesObjects & {
metadata: name: "dev-holos-infra"
apiObjectMap: OBJECTS.apiObjectMap
},
]
let Metadata = {
name: Holos
namespace: Namespace
labels: app: Holos
}
// OBJECTS represents the kubernetes api objects to manage.
let OBJECTS = #APIObjects & {
// Postgres
// Deployment
// VirtualService
apiObjects: ServiceAccount: holos: {
metadata: Metadata
imagePullSecrets: [{name: "kube-system-ecr-image-pull-creds"}]
}
apiObjects: PostgresCluster: holos: {
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
metadata: Metadata
spec: {
image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.1-0"
instances: [{
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
podAffinityTerm: {
labelSelector: matchLabels: "postgres-operator.crunchydata.com/cluster": "holos"
topologyKey: "kubernetes.io/hostname"
}
weight: 1
}]
dataVolumeClaimSpec: {
accessModes: ["ReadWriteOnce"]
resources: requests: storage: "1Gi"
}
name: "db"
replicas: 1
}]
port: 5432
postgresVersion: 16
users: [{
databases: ["holos"]
name: "holos"
options: "SUPERUSER"
}]
backups: pgbackrest: {
global: {
"archive-async": "y"
"archive-push-queue-max": "100MiB"
"spool-path": "/pgdata/backups"
}
image: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.47-2"
repos: [{
name: "repo1"
volume: volumeClaimSpec: {
accessModes: ["ReadWriteOnce"]
resources: requests: storage: "1Gi"
}
}]
}
}
}
apiObjects: Service: holos: {
apiVersion: "v1"
metadata: Metadata
spec: {
type: "ClusterIP"
selector: app: "holos"
ports: [{
appProtocol: "http2"
name: "http"
port: 3000
protocol: "TCP"
targetPort: 3000
}, {
appProtocol: "http"
name: "metrics"
port: 9090
protocol: "TCP"
targetPort: 9090
}]
}
}
apiObjects: VirtualService: holos: {
apiVersion: "networking.istio.io/v1beta1"
metadata: Metadata
spec: {
gateways: ["istio-ingress/default"]
hosts: [
"app.dev.holos.run",
"app.dev.\(#ClusterName).holos.run",
]
http: [{
match: [{
uri: prefix: "/ui"
}]
name: "ui"
route: [{
destination: {
host: "holos"
port: number: 3000
}
}]
}, {
name: "api"
route: [{
destination: {
host: "holos"
port: number: 3000
}
}]
}]
}
}
}

View File

@@ -0,0 +1,25 @@
package holos
// Platform level definition of a project.
#Project: {
name: string
// All projects have at least a prod and dev environment and stage.
// Omit the prod stage segment from hostnames. foo.holos.run not foo.prod.holos.run
stages: prod: stageSegments: []
environments: prod: stage: "prod"
// Omit the prod env segment from hostnames. foo.holos.run not prod.foo.holos.run
environments: prod: envSegments: []
stages: dev: _
environments: dev: stage: "dev"
// Omit the dev env segment from hostnames. foo.dev.holos.run not dev.foo.dev.holos.run
environments: dev: envSegments: []
// environments share the stage segments of their stage.
environments: [_]: {
stage: string
stageSegments: stages[stage].stageSegments
}
}

View File

@@ -1 +0,0 @@
package holos

View File

@@ -0,0 +1,143 @@
package holos
import "strings"
// #ProjectHosts represents all of the hosts associated with the project
// organized for use in Certificates, Gateways, VirtualServices.
#ProjectHosts: {
project: #Project
// Hosts map key fqdn to host to reduce into structs organized by stage,
// canonical name, etc... The flat nature and long list of properties is
// intended to make it straight forward to derive another struct for Gateways,
// VirtualServices, Certificates, AuthProxy cookie domains, etc...
Hosts: {
for Env in project.environments {
for Host in project.hosts {
// Global hostname, e.g. app.holos.run
let CertInfo = (#MakeCertInfo & {
host: Host
env: Env
domain: project.domain
clusterMap: project.clusters
}).CertInfo
"\(CertInfo.fqdn)": CertInfo
// Cluster hostname, e.g. app.east1.holos.run, app.west1.holos.run
for Cluster in project.clusters {
let CertInfo = (#MakeCertInfo & {
host: Host
env: Env
domain: project.domain
cluster: Cluster.name
clusterMap: project.clusters
}).CertInfo
"\(CertInfo.fqdn)": CertInfo
}
}
}
}
}
// #MakeCertInfo provides dns info for a certificate
// Refer to: https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#MakeCertInfo: {
host: #Host
env: #Environment
domain: string
cluster: string
clusterMap: #ClusterMap
let Stage = #StageInfo & {name: env.stage, project: env.project}
let Env = env
// DNS segments from left to right.
let EnvSegments = env.envSegments
WildcardSegments: [...string]
if len(env.envSegments) > 0 {
WildcardSegments: ["*"]
}
let HostSegments = [host.name]
let StageSegments = env.stageSegments
ClusterSegments: [...string]
if cluster != _|_ {
ClusterSegments: [cluster]
}
let DomainSegments = [domain]
// Assemble the segments
let FQDN = EnvSegments + HostSegments + StageSegments + ClusterSegments + DomainSegments
let WILDCARD = WildcardSegments + HostSegments + StageSegments + ClusterSegments + DomainSegments
let CANONICAL = HostSegments + StageSegments + DomainSegments
CertInfo: #CertInfo & {
fqdn: strings.Join(FQDN, ".")
wildcard: strings.Join(WILDCARD, ".")
canonical: strings.Join(CANONICAL, ".")
project: name: Env.project
stage: #StageOrEnvRef & {
name: Stage.name
slug: Stage.slug
namespace: Stage.namespace
}
env: #StageOrEnvRef & {
name: Env.name
slug: Env.slug
namespace: Env.namespace
}
if cluster != _|_ {
// Host is valid on a single cluster.
clusters: "\(cluster)": _
}
if cluster == _|_ {
// Host is valid on all project clusters.
clusters: clusterMap
}
NoAuthorizationPolicy: host.NoAuthorizationPolicy
}
}
// #CertInfo defines the attributes associated with a fully qualfied domain name
#CertInfo: {
// fqdn is the fully qualified domain name, never a wildcard.
fqdn: string
// canonical is the canonical name this name may be an alternate name for.
canonical: string
// wildcard may replace the left most segment fqdn with a wildcard to consolidate cert dnsNames. If not a wildcad, must be fqdn
wildcard: string
// Project, stage and env attributes for mapping and collecting.
project: name: string
stage: #StageOrEnvRef
env: #StageOrEnvRef
// clusters represents the cluster names the fqdn is valid on.
clusters: #ClusterMap
// hosts are always valid on the provisioner cluster
clusters: provisioner: _
// NoAuthorizationPolicy excludes the host from the auth proxy integrated with
// the default ingress Gateway.
NoAuthorizationPolicy: true | *false
}
#ClusterMap: [Name=string]: #Cluster & {name: Name}
#StageOrEnvRef: {
name: string
slug: string
namespace: string
}

View File

@@ -0,0 +1,45 @@
package holos
#ProjectTemplate: {
project: _
GatewayServers: _
// Sort GatewayServers by the tls credentialName to issue wildcards
let GatewayCerts = {
for FQDN, Server in GatewayServers {
let CertInfo = Server._CertInfo
// Sort into stage for the holos components, e.g. prod-iam-certs, dev-iam-certs
"\(CertInfo.stage.slug)": {
"\(Server.tls.credentialName)": #Certificate & {
// Store the dnsNames in a struct so they can be collected into a list
_dnsNames: "\(CertInfo.wildcard)": CertInfo.wildcard
metadata: name: CertInfo.canonical & Server.tls.credentialName
metadata: namespace: "istio-ingress"
spec: {
commonName: CertInfo.canonical
secretName: CertInfo.canonical & Server.tls.credentialName
dnsNames: [for x in _dnsNames {x}]
issuerRef: {
kind: "ClusterIssuer"
name: "letsencrypt"
}
}
}
}
}
}
// Resources to be managed on the provisioner cluster.
provisioner: resources: {
for stage in project.stages {
"\(stage.slug)-certs": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: Certificate: GatewayCerts[stage.slug]
}).apiObjectMap
}
}
}
}

View File

@@ -1,87 +1,77 @@
package holos
import "encoding/yaml"
import (
h "github.com/holos-run/holos/api/v1alpha1"
"encoding/yaml"
"strings"
)
// Platform level definition of a project.
#Project: {
name: string
// let SourceLoc = "project-template.cue"
// All projects have at least a prod environment and stage.
stages: prod: stageSegments: []
environments: prod: stage: "prod"
environments: prod: envSegments: []
stages: dev: _
environments: dev: stage: "dev"
environments: dev: envSegments: []
// Ensure at least the project name is a short hostname. Additional may be added.
hosts: (name): _
#ProjectTemplate: {
project: #Project
// environments share the stage segments of their stage.
environments: [_]: {
stage: string
stageSegments: stages[stage].stageSegments
// workload cluster resources
workload: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
// provisioner cluster resources
provisioner: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
}
// Reference Platform Project Template
#ProjectTemplate: {
project: #Project
let Project = project
// GatewayServers maps Gateway spec.servers #GatewayServer values indexed by stage then name.
let GatewayServers = {
// Initialize all stages, even if they have no environments.
for stage in project.stages {
(stage.name): {}
}
// ProjectHosts represents all of the hosts associated with a project indexed
// by FQDN with #CertInfo values. Slice and dice this struct as needed to
// work with hosts in the platform.
ProjectHosts: (#ProjectHosts & {project: Project}).Hosts
// For each stage, construct entries for the Gateway spec.servers.hosts field.
for env in project.environments {
(env.stage): {
let Env = env
let Stage = project.stages[env.stage]
for host in (#EnvHosts & {project: Project, env: Env}).hosts {
(host.name): #GatewayServer & {
hosts: [
"\(env.namespace)/\(host.name)",
// Allow the authproxy VirtualService to match the project.authProxyPrefix path.
"\(Stage.namespace)/\(host.name)",
]
port: host.port
tls: credentialName: host.name
tls: mode: "SIMPLE"
// GatewayServers maps Gateway spec.servers #GatewayServer values indexed by stage then name.
GatewayServers: {
for FQDN, Host in ProjectHosts {
// If the host is valid on the cluster being rendered
if Host.clusters[#ClusterName] != _|_ {
"\(FQDN)": #GatewayServer & {
_CertInfo: Host
hosts: [
"\(Host.env.namespace)/\(FQDN)",
// Allow the authproxy VirtualService to match the project.authProxyPrefix path.
"\(Host.stage.namespace)/\(FQDN)",
]
port: {
// NOTE: port names in servers must be unique: duplicate name https
name: "https-" + strings.Replace(FQDN, ".", "-", -1)
number: 443
protocol: "HTTPS"
}
tls: credentialName: Host.canonical
tls: mode: "SIMPLE"
}
}
}
}
// ClusterGatewayServers provides a struct of Gateway servers for the current cluster.
// ClusterDefaultGatewayServers provides a struct of Gateway servers for the current cluster.
// This is intended for Gateway/default to add all servers to the default gateway.
ClusterGatewayServers: {
ClusterDefaultGatewayServers: {
if project.clusters[#ClusterName] != _|_ {
for Stage in project.stages {
for server in GatewayServers[Stage.name] {
(server.port.name): server
}
}
GatewayServers
}
}
workload: resources: {
// Provide resources only if the project is managed on --cluster-name
// Provide resources only if the project is managed on the cluster specified
// by --cluster-name
if project.clusters[#ClusterName] != _|_ {
for stage in project.stages {
let Stage = stage
// Istio Gateway
"\(stage.slug)-gateway": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
for host in GatewayServers[stage.name] {
apiObjects: ExternalSecret: (host.tls.credentialName): metadata: namespace: "istio-ingress"
}
}).apiObjectMap
}
// Manage auth-proxy in each stage
if project.features.authproxy.enabled {
"\(stage.slug)-authproxy": #KubernetesObjects & {
@@ -114,90 +104,6 @@ import "encoding/yaml"
}
}
}
provisioner: resources: {
for stage in project.stages {
"\(stage.slug)-certs": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
for host in GatewayServers[stage.name] {
let CN = host.tls.credentialName
apiObjects: Certificate: (CN): #Certificate & {
metadata: name: CN
metadata: namespace: "istio-ingress"
spec: {
commonName: CN
dnsNames: [CN]
secretName: CN
issuerRef: {
kind: "ClusterIssuer"
name: "letsencrypt"
}
}
}
}
}).apiObjectMap
}
}
}
}
let HTTPBIN = {
name: string | *"httpbin"
project: #Project
env: #Environment
let Name = name
let Stage = project.stages[env.stage]
let Metadata = {
name: Name
namespace: env.namespace
labels: app: name
}
let Labels = {
"app.kubernetes.io/name": Name
"app.kubernetes.io/instance": env.slug
"app.kubernetes.io/part-of": env.project
"security.holos.run/authproxy": Stage.extAuthzProviderName
}
apiObjects: {
Deployment: (Name): #Deployment & {
metadata: Metadata
spec: selector: matchLabels: Metadata.labels
spec: template: {
metadata: labels: Metadata.labels & #IstioSidecar & Labels
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: containers: [{
name: Name
image: "quay.io/holos/mccutchen/go-httpbin"
ports: [{containerPort: 8080}]
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}}]
}
}
Service: (Name): #Service & {
metadata: Metadata
spec: selector: Metadata.labels
spec: ports: [
{port: 80, targetPort: 8080, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: Metadata
let Project = project
let Env = env
spec: hosts: [for host in (#EnvHosts & {project: Project, env: Env}).hosts {host.name}]
spec: gateways: ["istio-ingress/default"]
spec: http: [{route: [{destination: host: Name}]}]
}
}
}
// AUTHPROXY configures one oauth2-proxy deployment for each host in each stage of a project. Multiple deployments per stage are used to narrow down the cookie domain.
@@ -462,6 +368,65 @@ let AUTHPROXY = {
}
}
let HTTPBIN = {
name: string | *"httpbin"
project: #Project
env: #Environment
let Name = name
let Stage = project.stages[env.stage]
let Metadata = {
name: Name
namespace: env.namespace
labels: app: name
}
let Labels = {
"app.kubernetes.io/name": Name
"app.kubernetes.io/instance": env.slug
"app.kubernetes.io/part-of": env.project
"security.holos.run/authproxy": Stage.extAuthzProviderName
}
apiObjects: {
Deployment: (Name): #Deployment & {
metadata: Metadata
spec: selector: matchLabels: Metadata.labels
spec: template: {
metadata: labels: Metadata.labels & #IstioSidecar & Labels
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: containers: [{
name: Name
image: "quay.io/holos/mccutchen/go-httpbin"
ports: [{containerPort: 8080}]
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}}]
}
}
Service: (Name): #Service & {
metadata: Metadata
spec: selector: Metadata.labels
spec: ports: [
{port: 80, targetPort: 8080, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: Metadata
let Project = project
let Env = env
spec: hosts: [for host in (#EnvHosts & {project: Project, env: Env}).hosts {host.name}]
spec: gateways: ["istio-ingress/default"]
spec: http: [{route: [{destination: host: Name}]}]
}
}
}
// AUTHPOLICY configures the baseline AuthorizationPolicy and RequestAuthentication policy for each stage of each project.
let AUTHPOLICY = {
project: #Project

View File

@@ -1,12 +1,12 @@
package holos
import h "github.com/holos-run/holos/api/v1alpha1"
import "strings"
// #Projects is a map of all the projects in the platform.
#Projects: [Name=_]: #Project & {name: Name}
_Projects: #Projects
// The platform project is required and where platform services reside. ArgoCD, Grafana, Prometheus, etc...
#Projects: platform: _
@@ -59,7 +59,7 @@ import "strings"
}
}
// features is YAGNI maybe?
// Thes are useful to enable / disable.
features: [Name=string]: #Feature & {name: Name}
features: authproxy: _
features: httpbin: _
@@ -68,8 +68,13 @@ import "strings"
// #Cluster defines a cluster
#Cluster: name: string
// #Host defines a short hostname
#Host: name: string
#Host: {
// #Host defines a short hostname
name: string
// NoAuthorizationPolicy excludes the host from the auth proxy integrated with
// the default ingress Gateway.
NoAuthorizationPolicy: true | *false
}
#Environment: {
// name uniquely identifies the environment within the scope of the project.
@@ -91,15 +96,25 @@ import "strings"
name: string
cluster?: string
clusterSegments: [...string]
wildcard: true | *false
if cluster != _|_ {
clusterSegments: [cluster]
}
let SEGMENTS = envSegments + [name] + stageSegments + clusterSegments + [#Platform.org.domain]
_EnvSegments: [...string]
if wildcard {
if len(envSegments) > 0 {
_EnvSegments: ["*"]
}
}
if !wildcard {
_EnvSegments: envSegments
}
let SEGMENTS = _EnvSegments + [name] + stageSegments + clusterSegments + [_Projects[project].domain]
let NAMESEGMENTS = ["https"] + SEGMENTS
host: {
name: strings.Join(SEGMENTS, ".")
port: {
name: strings.Replace(strings.Join(NAMESEGMENTS, "-"), ".", "-", -1)
name: strings.Replace(strings.Replace(strings.Join(NAMESEGMENTS, "-"), ".", "-", -1), "*", "wildcard", -1)
number: 443
protocol: "HTTPS"
}
@@ -107,17 +122,26 @@ import "strings"
}
}
#Stage: {
#StageInfo: {
name: string
project: string
slug: "\(name)-\(project)"
// namespace is the system namespace for the project stage
namespace: "\(name)-\(project)-system"
}
#Stage: {
#StageInfo
name: string
project: string
namespace: string
slug: string
// Manage a system namespace for each stage
namespaces: [Name=_]: name: Name
namespaces: (namespace): _
namespaces: "\(namespace)": _
// stageSegments are the stage portion of the dns segments
stageSegments: [...string] | *[name]
stageSegments: [] | *[name]
// authProxyClientID is the ClientID registered with the oidc issuer.
authProxyClientID: string
// extAuthzProviderName is the provider name in the mesh config
@@ -130,20 +154,6 @@ import "strings"
enabled: true | *false
}
#ProjectTemplate: {
project: #Project
// workload cluster resources
workload: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
// provisioner cluster resources
provisioner: resources: [Name=_]: h.#KubernetesObjects & {
metadata: name: Name
}
}
// #EnvHosts provides hostnames given a project and environment.
// Refer to https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#EnvHosts: {
@@ -166,7 +176,7 @@ import "strings"
}
// #StageDomains provides hostnames given a project and stage. Primarily for the
// auth proxy.
// auth proxy cookie domains.
// Refer to https://github.com/holos-run/holos/issues/66#issuecomment-2027562626
#StageDomains: {
// names are the leading prefix names to create hostnames for.

View File

@@ -15,6 +15,7 @@ import (
crt "cert-manager.io/certificate/v1"
gw "networking.istio.io/gateway/v1beta1"
vs "networking.istio.io/virtualservice/v1beta1"
dr "networking.istio.io/destinationrule/v1beta1"
ra "security.istio.io/requestauthentication/v1"
ap "security.istio.io/authorizationpolicy/v1"
pg "postgres-operator.crunchydata.com/postgrescluster/v1beta1"
@@ -77,7 +78,9 @@ _apiVersion: "holos.run/v1alpha1"
#Job: #NamespaceObject & batchv1.#Job
#CronJob: #NamespaceObject & batchv1.#CronJob
#Deployment: #NamespaceObject & appsv1.#Deployment
#StatefulSet: #NamespaceObject & appsv1.#StatefulSet
#VirtualService: #NamespaceObject & vs.#VirtualService
#DestinationRule: #NamespaceObject & dr.#DestinationRule
#RequestAuthentication: #NamespaceObject & ra.#RequestAuthentication
#AuthorizationPolicy: #NamespaceObject & ap.#AuthorizationPolicy
#Certificate: #NamespaceObject & crt.#Certificate
@@ -182,7 +185,7 @@ _apiVersion: "holos.run/v1alpha1"
pool?: string
// region is the geographic region of the cluster.
region?: string
// primary is true if name matches the primaryCluster name
// primary is true if the cluster is the primary cluster among a group of related clusters.
primary: bool
}
@@ -219,6 +222,7 @@ _apiVersion: "holos.run/v1alpha1"
primary: false
}
}
// TODO: Remove stages, they're in the subdomain of projects.
stages: [ID=_]: {
name: string & ID
environments: [...{name: string}]
@@ -226,9 +230,11 @@ _apiVersion: "holos.run/v1alpha1"
projects: [ID=_]: {
name: string & ID
}
// TODO: Remove services, they're in the subdomain of projects.
services: [ID=_]: {
name: string & ID
}
// authproxy configures the auth proxy attached to the default ingress gateway in the istio-ingress namespace.
authproxy: #AuthProxySpec & {
namespace: "istio-ingress"
@@ -277,29 +283,6 @@ _apiVersion: "holos.run/v1alpha1"
idTokenHeader: string | *"x-oidc-id-token"
}
// ManagedNamespace is a namespace to manage across all clusters in the holos platform.
#ManagedNamespace: {
namespace: {
metadata: {
name: string
labels: [string]: string
}
}
// clusterNames represents the set of clusters the namespace is managed on. Usually all clusters.
clusterNames: [...string]
for cluster in clusterNames {
clusters: (cluster): name: cluster
}
}
// #ManagedNamepsaces is the union of all namespaces across all cluster types and optional services.
// Holos adopts the namespace sameness position of SIG Multicluster, refer to https://github.com/kubernetes/community/blob/dd4c8b704ef1c9c3bfd928c6fa9234276d61ad18/sig-multicluster/namespace-sameness-position-statement.md
#ManagedNamespaces: {
[Name=_]: #ManagedNamespace & {
namespace: metadata: name: Name
}
}
// #Backups defines backup configuration.
// TODO: Consider the best place for this, possibly as part of the site platform config. This represents the primary location for backups.
#Backups: {

14
docs/runbooks/argocd.md Normal file
View File

@@ -0,0 +1,14 @@
# ArgoCD
Create the deploy key secret in the management cluster.
```bash
tmp="$(mktemp -d)"
(cd $tmp && ssh-keygen -t ed25519 -f sshPrivateKey -m pem -C argocd -N '')
echo git@github.com:holos-run/holos-infra.git > "${tmp}/url"
holos create secret -n argocd --append-hash=false creds-holos-infra --from-file $tmp
rm -rf "$tmp"
```
When syncing the secret, the ExternalSecret needs to set the label
`argocd.argoproj.io/secret-type: repo-creds`.

View File

@@ -0,0 +1,97 @@
# PostgresCluster Backups
This document describes how the S3 bucket for `PostgresCluster` backups is configured. These buckets are configured both for ZITADEL and for Holos
Server and are applicable to any service in Holos that stores data in a pgo `PostgresCluster` resource.
## Create the Bucket
Name: `holos-zitadel-backups` for `zitadel`
Name: `holos-server-backups` for `holos server`
> [!NOTE]
> The settings below match the default settings recommended by AWS.
Object Ownership: `ACLs disabled` (recommended) Checked.
Block Public Access settings for this bucket: **`Block all public access`** Checked.
Bucket Versioning: `Disable`
Default encryption: `Server-side encryption with Amazon S3 managed keys (SSE-S3)`
Bucket Key: `Enable`
Object Lock: `Disable`
## Create an IAM Policy
Create one IAM Policy for each bucket to grant full access to the bucket. Replace the resource with each bucket name.
Name: `holos-zitadel-backups` for `zitadel`
Name: `holos-server-backups` for `holos server`
Description: `Read and write access to a specific bucket for pgrest operating within a pgo PostgresCluster.`
Policy JSON:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListAllMyBuckets"
],
"Resource": "arn:aws:s3:::*"
},
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::holos-zitadel-backups",
"arn:aws:s3:::holos-zitadel-backups/*"
]
}
]
}
```
## Create an IAM Group
Create an IAM Group to attach the policy granting access to the bucket.
Name: `holos-zitadel-backups` for `zitadel`
Attach permission policies: `holos-zitadel-backups`
Name: `holos-server-backups` for `holos server`
Attach permission policies: `holos-server-backups`
## Create the IAM User
Create an IAM User entity for each PostgresCluster. Do not provide user access to the AWS Management Console.
Name: `holos-zitadel-backups` for `zitadel`
Group: `holos-zitadel-backups`
Name: `holos-server-backups` for `holos server`
Group: `holos-server-backups`
## Create an Access Key
Create an access key for `pgbackrest` associated with the `PostgresCluster`.
Description:
> Used by pgbackrest associated with the PostgresCluster resource. Refer to the PostgresCluster resource pgbackrest.cofiguration.secret.name for the stored location of the access key. Synced from the Management Cluster using an ExternalSecret.
## Create the Secret
Create a `Secret` in the holos management cluster usable by pgbackrest. This is a secret with a single key, `s3.conf` with the following format:
```
[global]
repo2-cipher-pass=
repo2-s3-key=
repo2-s3-key-secret=
repo3-cipher-pass=
repo3-s3-key=
repo3-s3-key-secret=
```
> [!NOTE]
> Use the same values for repo2 and repo3. The purpose is to make space for migrating if need be in the future.
Generate the cipher pass using. This password is used to encrypt all backups using client side before the backup is written to the bucket.
```
tr -dc A-Za-z0-9 </dev/urandom | head -c 64
```
Store the secret into the management cluster:
```
holos create secret --namespace zitadel holos-zitadel-backups \
--append-hash=false --from-file .
```
```
holos create secret --namespace holos holos-server-backups \
--append-hash=false --from-file .
```

View File

@@ -0,0 +1,92 @@
# PG Bouncer
Every few days ZITADEL fails. The problem seems to be related to pgbouncer not
being able to resolve DNS. Restarting the pgbouncer pod fixes the issue.
See [How to load-balance queries between several servers?](https://www.pgbouncer.org/faq.html#how-to-load-balance-queries-between-several-servers)
> [!NOTE]
> DNS round-robin. Use several IPs behind one DNS name. PgBouncer does not look up DNS each time a new connection is launched. Instead, it caches all IPs and does round-robin internally. Note: if there are more than 8 IPs behind one name, the DNS backend must support the EDNS0 protocol. See README for details.
## Workaround
```sh
# Get the tls based creds to bypass oidc
(cd ~/.kube && holos get secret core2-kubeconfig-admin --print-key kubeconfig.admin > core2.admin)
export KUBECONFIG=$HOME/.kube/core2.admin
# Restart pgbouncer
kubectl -n prod-iam rollout restart deployment zitadel-pgbouncer
```
## Symptom logs
```sh
kubectl -n prod-iam logs -c pgbouncer -l postgres-operator.crunchydata.com/role=pgbouncer
```
```txt
2024-05-08 17:56:11.424 UTC [7] LOG S-0x559b03f90ff0: zitadel/zitadel@10.110.109.110:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384/ECDH=prime256v1
2024-05-08 17:56:11.429 UTC [7] LOG S-0x559b03f92820: zitadel/zitadel@10.110.109.110:5432 new connection to server (from 10.244.5.38:53658)
2024-05-08 17:56:11.435 UTC [7] LOG S-0x559b03f92820: zitadel/zitadel@10.110.109.110:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384/ECDH=prime256v1
2024-05-08 17:56:11.476 UTC [7] LOG C-0x559b03f7a610: zitadel/zitadel@10.244.2.89:34932 closing because: client close request (age=440s)
2024-05-08 17:56:19.708 UTC [7] LOG stats: 15 xacts/s, 42 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 6159 B/s, out 6124 B/s, xact 3930 us, query 869 us, wait 490 us
[msg] Nameserver 10.96.0.10:53 is back up
2024-05-08 17:57:09.366 UTC [7] LOG C-0x559b03f7a610: zitadel/zitadel@10.244.3.187:58674 login attempt: db=zitadel user=zitadel tls=TLSv1.3/TLS_AES_256_GCM_SHA384
2024-05-08 17:57:09.391 UTC [7] LOG C-0x559b03f7a610: zitadel/zitadel@10.244.3.187:58674 closing because: client close request (age=0s)
2024-05-08 17:57:19.709 UTC [7] LOG stats: 9 xacts/s, 24 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 2870 B/s, out 3018 B/s, xact 4147 us, query 958 us, wait 23 us
2024-05-08 17:58:19.708 UTC [7] LOG stats: 12 xacts/s, 32 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 3861 B/s, out 3533 B/s, xact 3843 us, query 853 us, wait 0 us
2024-05-08 17:56:11.411 UTC [8] LOG S-0x55a894e36650: zitadel/_crunchypgbouncer@10.110.109.110:5432 new connection to server (from 10.244.3.227:58984)
2024-05-08 17:56:11.411 UTC [8] LOG S-0x55a894e37920: zitadel/zitadel@10.110.109.110:5432 new connection to server (from 10.244.3.227:58992)
2024-05-08 17:56:11.418 UTC [8] LOG S-0x55a894e37920: zitadel/zitadel@10.110.109.110:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384/ECDH=prime256v1
2024-05-08 17:56:11.420 UTC [8] LOG S-0x55a894e36650: zitadel/_crunchypgbouncer@10.110.109.110:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384/ECDH=prime256v1
2024-05-08 17:56:11.438 UTC [8] LOG S-0x55a894e35b90: zitadel/zitadel@10.110.109.110:5432 new connection to server (from 10.244.3.227:59004)
2024-05-08 17:56:11.445 UTC [8] LOG S-0x55a894e35b90: zitadel/zitadel@10.110.109.110:5432 SSL established: TLSv1.3/TLS_AES_256_GCM_SHA384/ECDH=prime256v1
2024-05-08 17:56:17.148 UTC [8] LOG stats: 9 xacts/s, 27 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 3236 B/s, out 2826 B/s, xact 5224 us, query 910 us, wait 1182 us
[msg] Nameserver 10.96.0.10:53 is back up
2024-05-08 17:57:17.145 UTC [8] LOG stats: 10 xacts/s, 31 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 4342 B/s, out 4305 B/s, xact 4536 us, query 776 us, wait 0 us
2024-05-08 17:58:17.149 UTC [8] LOG stats: 5 xacts/s, 15 queries/s, 0 client parses/s, 0 server parses/s, 0 binds/s, in 1641 B/s, out 1582 B/s, xact 7819 us, query 1426 us, wait 0 us
```
## Relevant Configuration
`/etc/pgbouncer/pgbouncer.ini` is empty.
```
bash-4.4$ cat /etc/pgbouncer/~postgres-operator.ini
# Generated by postgres-operator. DO NOT EDIT.
# Your changes will not be saved.
[pgbouncer]
%include /etc/pgbouncer/pgbouncer.ini
[pgbouncer]
auth_file = /etc/pgbouncer/~postgres-operator/users.txt
auth_query = SELECT username, password from pgbouncer.get_auth($1)
auth_user = _crunchypgbouncer
client_tls_ca_file = /etc/pgbouncer/~postgres-operator/frontend-ca.crt
client_tls_cert_file = /etc/pgbouncer/~postgres-operator/frontend-tls.crt
client_tls_key_file = /etc/pgbouncer/~postgres-operator/frontend-tls.key
client_tls_sslmode = require
conffile = /etc/pgbouncer/~postgres-operator.ini
ignore_startup_parameters = extra_float_digits
listen_addr = *
listen_port = 5432
server_tls_ca_file = /etc/pgbouncer/~postgres-operator/backend-ca.crt
server_tls_sslmode = verify-full
unix_socket_dir =
[databases]
* = host=zitadel-primary port=5432
```
### [host](https://www.pgbouncer.org/config.html#host)
> Host name or IP address to connect to. Host names are resolved at connection time, the result is cached per dns_max_ttl parameter. When a host names resolution changes, existing server connections are automatically closed when they are released (according to the pooling mode), and new server connections immediately use the new resolution. If DNS returns several results, they are used in a round-robin manner.
### dns_max_ttl
[dns_max_ttl](https://www.pgbouncer.org/config.html#dns_max_ttl)
How long DNS lookups can be cached. The actual DNS TTL is ignored.
Default: 15.0 (seconds)

View File

@@ -0,0 +1,25 @@
# Namespaces
Holos follows the [Namespace Sameness - Sig Multicluster Position][1]. A
namespace is the same on all clusters within the scope of a platform.
Namespaces are also security boundaries for role based access control. As such,
permission to read a secret in a namespace means the secret is readable on all
clusters in the platform.
When adding a component to a platform, create a namespace using the following
process. This ensures a namespace scoped `SecretStore` is created to sync
`ExternalSecret` resources from the management cluster.
1. Add a new project to the `_Projects` struct in `platform.cue`.
2. Add the namespace to the `spec.namespaces` field of the project.
3. Render the platform
4. Apply the `namespaces` component to the management cluster
5. Apply the `eso-creds-manager` component to the management cluster to create the `eso-reader` ksa for the namespace `SecretStore`
6. Apply the `namespaces` component to the workload clusters
7. On the workload cluster, run the job to fetch the eso-reader creds: `kubectl create job -n holos-system --from=cronjob/eso-creds-refresher eso-creds-refresher-$(date +%s)`
8. Apply the secretstores component to the workload cluster.
Your namespace is created and you have the ability to create secrets in the management cluster and pull them using ExternalSecret resources. (edited)
[1]: https://github.com/kubernetes/community/blob/dd4c8b704ef1c9c3bfd928c6fa9234276d61ad18/sig-multicluster/namespace-sameness-position-statement.md

View File

@@ -0,0 +1,31 @@
# Workload Identity
When a new workload cluster is provisioned, allow it to access the Management
Cluster using workload identity. This is necessary for the
`eso-creds-refresher` component and `Job` that executes in each workload
cluster, which in turn enables the `SecretStore` in each namespace to sync
secrets.
Build the cluster with Cluster API.
See https://github.com/holos-run/holos-infra/blob/main/hack/capi/eks/aws2/aws2-managedmachinepool.yaml#L81-L84
## Workload Identity Provider
Add the Cluster as a workload identity provider to the `holos-ops` gcp project.
Pool: [holos](https://console.cloud.google.com/iam-admin/workload-identity-pools/pool/holos?organizationId=358674006047&project=holos-ops)
Name: `k8s-aws1`, `k8s-aws2`, etc...
### Issuer URL:
```
kubectl create -n default token default | cut -d. -f2 | base64 -d | jq -r .iss
```
### Audience
Use the default audience.
### Attribute Mapping
| Google | OIDC |
| -------------------------------- | ------------------------------------------------------ |
| `google.subject` | `assertion.sub` |
| `attribute.service_account_name` | `assertion['kubernetes.io']['serviceaccount']['name']` |
| `attribute.uid` | `assertion['kubernetes.io']['serviceaccount']['uid']` |
| `attribute.pod` | `assertion['kubernetes.io']['pod']['name']` |

202
go.mod
View File

@@ -5,27 +5,35 @@ go 1.21.5
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1
connectrpc.com/connect v1.16.0
connectrpc.com/grpcreflect v1.2.0
connectrpc.com/otelconnect v0.7.0
connectrpc.com/validate v0.1.0
cuelang.org/go v0.8.0
entgo.io/ent v0.13.1
github.com/bufbuild/buf v1.30.1
github.com/choria-io/machine-room v0.0.0-20240417064836-c604da2f005e
github.com/coreos/go-oidc/v3 v3.10.0
github.com/fullstorydev/grpcurl v1.9.1
github.com/go-jose/go-jose/v3 v3.0.3
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/uuid v1.5.0
github.com/int128/kubelogin v1.28.0
github.com/jackc/pgx/v5 v5.5.5
github.com/lmittmann/tint v1.0.4
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.9
github.com/mattn/go-runewidth v0.0.15
github.com/mennanov/fieldmask-utils v1.1.2
github.com/olekukonko/tablewriter v0.0.5
github.com/prometheus/client_golang v1.19.0
github.com/rogpeppe/go-internal v1.12.0
github.com/sethvargo/go-retry v0.2.4
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.22.0
golang.org/x/tools v0.19.0
google.golang.org/protobuf v1.33.0
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.24.0
golang.org/x/tools v0.20.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa
google.golang.org/protobuf v1.33.1-0.20240408130810-98873a205002
honnef.co/go/tools v0.4.7
k8s.io/api v0.29.2
k8s.io/apimachinery v0.29.2
k8s.io/client-go v0.29.2
@@ -36,72 +44,212 @@ require (
require (
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Freman/eventloghook v0.0.0-20191003051739-e4d803b6b48b // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protovalidate-go v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bufbuild/protocompile v0.10.0 // indirect
github.com/bufbuild/protovalidate-go v0.6.0 // indirect
github.com/bufbuild/protoyaml-go v0.1.8 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/choria-io/fisk v0.6.2 // indirect
github.com/choria-io/go-choria v0.28.1-0.20240416190746-b3bf9c7d5a45 // indirect
github.com/choria-io/go-updater v0.1.0 // indirect
github.com/choria-io/stream-replicator v0.8.3-0.20230503130504-86152f798aec // indirect
github.com/choria-io/tokens v0.0.4-0.20240316144214-a929d9325d48 // indirect
github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v26.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v26.0.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/proto v1.10.0 // indirect
github.com/envoyproxy/go-control-plane v0.12.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/expr-lang/expr v1.16.4 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/fgprof v0.9.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.17.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.19.1 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240416155748-26353dc0451f // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d // indirect
github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 // indirect
github.com/goss-org/goss v0.4.6 // indirect
github.com/gosuri/uilive v0.0.4 // indirect
github.com/gosuri/uiprogress v0.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/guptarohit/asciigraph v0.7.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/int128/listener v1.1.0 // indirect
github.com/int128/oauth2cli v1.14.0 // indirect
github.com/int128/oauth2dev v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect
github.com/jhump/protoreflect v1.16.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/looplab/fsm v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/mountinfo v0.7.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/jsm.go v0.1.1 // indirect
github.com/nats-io/jwt/v2 v2.5.5 // indirect
github.com/nats-io/nats-server/v2 v2.10.14 // indirect
github.com/nats-io/nats.go v1.34.1 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
github.com/onsi/gomega v1.31.1 // indirect
github.com/oleiade/reflections v1.0.1 // indirect
github.com/onsi/ginkgo/v2 v2.17.1 // indirect
github.com/onsi/gomega v1.32.0 // indirect
github.com/open-policy-agent/opa v0.63.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shirou/gopsutil/v3 v3.24.3 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xlab/tablewriter v0.0.0-20160610135559-80b567a11ad5 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.16.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.25.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.25.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/grpc v1.62.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

872
go.sum

File diff suppressed because it is too large Load Diff

22
hack/choria/gen-machine-signer Executable file
View File

@@ -0,0 +1,22 @@
#! /bin/bash
#
# build github.com/choria-io/go-choria with go build -trimpath -o choria -ldflags "-w" ./
# Refer to https://github.com/ripienaar/choria-compose/blob/main/setup.sh#L41
# Refer to https://github.com/holos-run/holos-infra/blob/v0.60.4/experiments/components/holos-saas/initialize/setup
# choria jwt keys machine-signer.seed machine-signer.public
set -euo pipefail
PARENT="$(cd "$(dirname "$0")" && pwd)"
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
mkdir machine-signer
cd machine-signer
choria jwt keys machine-signer.seed machine-signer.public
holos create secret machine-signer --from-file .

5
hack/choria/initialize/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/issuer/
/provisioner/
/broker/
/customers/
/agents/

View File

@@ -0,0 +1,20 @@
Initialize machine room provisioning credentials
When you want the holos controller to provision while operating in the current
working directory, run:
1. `init-choria-provisioner-creds` to populate secrets in the Holos
Provisioner Cluster (not to be confused with the Choria Provisioner).
2. `make-provisioning-jwt` to issue a `provisioning.jwt` file for `holos
controller` to use.
3. `holos controller --config=agent.cfg` to read `provisioning.jwt` and write
the provisioned config file and credentials to the current directory.
Expect the controller to provision.
Setup Notes:
The holos server flag `--provisioner-seed` must match the issuer.seed value.
To get the correct value to configure for holos server:
holos get secret choria-issuer --print-key=issuer.seed --namespace $NAMESPACE

View File

@@ -0,0 +1,64 @@
#! /bin/bash
#
export BROKER_PASSWORD="$(LC_ALL=C tr -dc "[:alpha:]" </dev/random | tr '[:upper:]' '[:lower:]' | head -c 32)"
export PROVISIONER_TOKEN="$(LC_ALL=C tr -dc "[:alpha:]" </dev/random | tr '[:upper:]' '[:lower:]' | head -c 32)"
set -xeuo pipefail
# Make sure gomplate is available
gomplate --version
PARENT="$(cd $(dirname "$0") && pwd)"
TOPLEVEL="$(cd "${PARENT}" && git rev-parse --show-toplevel)"
: "${NAMESPACE:=jeff-holos}"
export NAMESPACE
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
# Generate Secrets
# Create organization issuer
mkdir issuer
choria jwt keys "./issuer/issuer.seed" "./issuer/issuer.public"
ISSUER="$(<issuer/issuer.public)"
export ISSUER
# Provisioner token used for ???
mkdir provisioner
echo -n "${PROVISIONER_TOKEN}" > ./provisioner/token
# Provisioner signer
choria jwt keys ./provisioner/signer.seed ./provisioner/signer.public
choria jwt client ./provisioner/signer.jwt provisioner_signer ./issuer/issuer.seed \
--public-key "$(<provisioner/signer.public)" --server-provisioner --validity $((100*365))d --issuer
# Provisioner Secret
mkdir -p provisioner/secret
gomplate --input-dir "${PARENT}/templates/provisioner" --output-dir ./provisioner/secret/
cp ./provisioner/signer.seed ./provisioner/secret/signer.seed
cp ./provisioner/signer.jwt ./provisioner/secret/signer.jwt
# Provisioner Broker
mkdir broker
choria jwt keys ./broker/broker.seed ./broker/broker.public
choria jwt server ./broker/broker.jwt broker.holos.local "$(<broker/broker.public)" ./issuer/issuer.seed \
--org choria \
--collectives choria \
--subjects 'choria.node_metadata.>'
gomplate --input-dir "${PARENT}/templates/broker/" --output-dir ./broker/
echo -n "${BROKER_PASSWORD}" > ./broker/password
mkdir agents
choria jwt keys ./agents/signer.seed ./agents/signer.public
# Now save the secrets
holos create secret --append-hash=false --namespace $NAMESPACE choria-issuer --from-file=issuer
holos create secret --append-hash=false --namespace $NAMESPACE choria-broker --from-file=broker
holos create secret --append-hash=false --namespace $NAMESPACE choria-provisioner --from-file=provisioner/secret
holos create secret --append-hash=false --namespace $NAMESPACE choria-agents --from-file=agents

View File

@@ -0,0 +1,50 @@
#! /bin/bash
#
# Make a provisioner.jwt and put it in the current directory.
#
# Use the provisioner.jwt with `holos controller --config=controller.cfg` which
# will read the jwt from the same directory as the config file.
#
# Refer to Arri's
# [setup.sh](https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/setup.sh#L41)
# And our own nites at https://github.com/holos-run/holos/issues/142
PARENT="$(cd $(dirname "$0") && pwd)"
OUTDIR="$(pwd)"
: "${NAMESPACE:=jeff-holos}"
tmpdir="$(mktemp -d)"
finish() {
[[ -d "$tmpdir" ]] && rm -rvf "$tmpdir"
}
trap finish EXIT
cd "$tmpdir"
set -xeuo pipefail
# e.g. jeff.provision.dev.k2.holos.run
#
kubectl -n $NAMESPACE get virtualservice choria-broker-wss -o json > vs.json
jq --exit-status -r '.spec.hosts[0]' vs.json > host
# Get the issuer.seed
holos -n $NAMESPACE get secret choria-issuer --to-file issuer.seed
# Get the provisioner token to embed in the provisioning.jwt file.
holos -n $NAMESPACE get secret choria-provisioner --to-file token
# The --token flag value must be the same value set in the token field of provisioner.yaml
# Refer to https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/setup.sh#L41
# Refer to https://github.com/ripienaar/machine-room-mvp/blob/main/example/setup/templates/provisioner/provisioner.yaml#L6
choria jwt prov provisioning.jwt "issuer.seed" \
--token "$(<token)" \
--urls wss://$(<host):443 \
--default \
--protocol-v2 \
--insecure \
--update \
--validity 30d \
--extensions '{}'
cp provisioning.jwt "${OUTDIR}/"

View File

@@ -0,0 +1,23 @@
#! /bin/bash
#
# This script resets the choria config for a Namespace
PARENT="$(cd $(dirname "$0") && pwd)"
: "${NAMESPACE:=jeff-holos}"
export NAMESPACE
set -xeuo pipefail
KUBECONFIG=$HOME/.holos/kubeconfig.provisioner kubectl delete secret -n jeff-holos choria-agents choria-broker choria-provisioner choria-issuer
"${PARENT}/init-choria-provisioner-creds"
stamp="$(date)"
kubectl -n $NAMESPACE annotate externalsecret choria-broker secret.holos.run/refresh="$stamp" --overwrite
kubectl -n $NAMESPACE annotate externalsecret choria-provisioner secret.holos.run/refresh="$stamp" --overwrite
kubectl -n $NAMESPACE wait --for='jsonpath={.status.conditions[?(@.type=="Ready")].status}=True' externalsecret choria-provisioner choria-broker
kubectl -n $NAMESPACE rollout restart statefulset choria-broker
kubectl -n $NAMESPACE rollout restart deployment choria-provisioner

View File

@@ -0,0 +1,23 @@
loglevel = info
plugin.choria.stats_address = 0.0.0.0
plugin.choria.stats_port = 8222
plugin.choria.broker_network = true
plugin.choria.network.client_port = 4222
plugin.choria.network.peer_port = 5222
plugin.choria.network.system.user = system
plugin.choria.network.system.password = system
plugin.choria.network.peers = nats://choria-broker-0.choria-broker:5222,nats://choria-broker-1.choria-broker:5222,nats://choria-broker-2.choria-broker:5222
plugin.choria.use_srv = false
plugin.choria.network.websocket_port = 4333
plugin.security.provider = choria
# NOTE: plugin.security.choria.ca must not be set or provisioning will fail
# with a unhandled choria_provisioning purpose token error
plugin.security.choria.certificate = /etc/choria-tls/tls.crt
plugin.security.choria.key = /etc/choria-tls/tls.key
plugin.security.choria.token_file = /etc/choria/broker.jwt
plugin.security.choria.seed_file = /etc/choria/broker.seed
plugin.choria.network.provisioning.client_password = {{ .Env.BROKER_PASSWORD }}
plugin.security.issuer.names = choria
plugin.security.issuer.choria.public = {{ .Env.ISSUER }}

View File

@@ -0,0 +1 @@
{{ .Env.ISSUER -}}

View File

@@ -0,0 +1,7 @@
plugin.security.provider = choria
plugin.security.choria.token_file = /etc/provisioner/signer.jwt
plugin.security.choria.seed_file = /etc/provisioner/signer.seed
identity = provisioner_signer
plugin.choria.middleware_hosts = choria-broker-0.choria-broker:4222,choria-broker-1.choria-broker:4222,choria-broker-2.choria-broker:4222

View File

@@ -0,0 +1,9 @@
#! /bin/bash
#
set -xeuo pipefail
mkdir -p /home/choria/bin
install -m 0755 /etc/provisioner/helper.rb /home/choria/bin/helper.rb
exec /usr/sbin/choria-provisioner --config=/etc/provisioner/provisioner.yaml --choria-config=/etc/provisioner/choria.cfg

View File

@@ -0,0 +1,134 @@
#!/usr/bin/ruby
require "json"
require "yaml"
require "base64"
require "net/http"
require "openssl"
def parse_input
input = STDIN.read
begin
File.open("/tmp/request.json", "w") {|f| f.write(input)}
rescue Exception
end
request = JSON.parse(input)
request["inventory"] = JSON.parse(request["inventory"])
request
end
def validate!(request, reply)
if request["identity"] && request["identity"].length == 0
reply["msg"] = "No identity received in request"
reply["defer"] = true
return false
end
unless request["ed25519_pubkey"]
reply["msg"] = "No ed15519 public key received"
reply["defer"] = true
return false
end
unless request["ed25519_pubkey"]
reply["msg"] = "No ed15519 directory received"
reply["defer"] = true
return false
end
if request["ed25519_pubkey"]["directory"].length == 0
reply["msg"] = "No ed15519 directory received"
reply["defer"] = true
return false
end
true
end
def publish_reply(reply)
begin
File.open("/tmp/reply.json", "w") {|f| f.write(reply.to_json)}
rescue Exception
end
puts reply.to_json
end
def publish_reply!(reply)
publish_reply(reply)
exit
end
def set_config!(request, reply)
# stub data the helper will fetch from the saas
customers = {
"one" => {
:brokers => "nats://managed.example.net:9222", # whoever is the leader for this site
:site => "customer_one",
:source => {
:host => "nats://cust_one:s3cret@saas-nats.choria.local",
}
}
}
customer = request["jwt"]["extensions"]["customer"]
brokers = customers[customer][:brokers]
source = customers[customer][:source]
reply["configuration"].merge!(
"identity" => request["identity"],
"loglevel" => "warn",
"plugin.choria.server.provision" => "false",
"plugin.choria.middleware_hosts" => brokers,
"plugin.security.issuer.names" => "choria",
"plugin.security.issuer.choria.public" => "{{ .Env.ISSUER }}",
"plugin.security.provider" => "choria",
"plugin.security.choria.token_file" => File.join(request["ed25519_pubkey"]["directory"], "server.jwt"),
"plugin.security.choria.seed_file" => File.join(request["ed25519_pubkey"]["directory"], "server.seed"),
"machine_room.role" => "leader",
"machine_room.site" => customers[customer][:site],
"machine_room.source.host" => source[:host],
)
reply["server_claims"].merge!(
"exp" => 5*60*60*24*365,
"pub_subjects" => [">"],
"permissions" => {
"streams" => true,
"submission" => true,
"service_host" => true,
}
)
end
reply = {
"defer" => false,
"msg" => "",
"certificate" => "",
"ca" => "",
"configuration" => {},
"server_claims" => {}
}
begin
request = parse_input
reply["msg"] = "Validating"
unless validate!(request, reply)
publish_reply!(reply)
end
set_config!(request, reply)
reply["msg"] = "Done"
publish_reply!(reply)
rescue SystemExit
rescue Exception
reply["msg"] = "Unexpected failure during provisioning: %s: %s" % [$!.class, $!.to_s]
reply["defer"] = true
publish_reply!(reply)
end

View File

@@ -0,0 +1,17 @@
workers: 4
interval: 1m
logfile: /dev/stdout
loglevel: info
# The entrypoint script installs this helper script.
helper: /home/choria/bin/helper.rb
token: "{{ .Env.PROVISIONER_TOKEN }}"
choria_insecure: false
site: holos
broker_provisioning_password: "{{ .Env.BROKER_PASSWORD }}"
jwt_verify_cert: "{{ .Env.ISSUER }}"
jwt_signing_key: /etc/provisioner/signer.seed
jwt_signing_token: /etc/provisioner/signer.jwt
features:
jwt: true
ed25519: true

View File

@@ -0,0 +1 @@
{{ .Env.PROVISIONER_TOKEN -}}

16
hack/setup/bare Executable file
View File

@@ -0,0 +1,16 @@
#! /bin/bash
set -euo pipefail
TOPLEVEL="$(cd $(dirname "$0") && git rev-parse --show-toplevel)"
host="jeff.app.dev.k2.holos.run:443"
read -p "Reset all data in $host? " choice
case "$choice" in
y|Y) echo "proceeding...";;
*) exit 1;;
esac
grpcurl -H "x-oidc-id-token: $(holos token)" $host holos.v1alpha1.SystemService.DropTables
grpcurl -H "x-oidc-id-token: $(holos token)" $host holos.v1alpha1.SystemService.SeedDatabase

View File

@@ -3,5 +3,6 @@ USER root
WORKDIR /app
ADD bin bin
RUN chown -R app: /app
USER app
# Kubernetes requires the user to be numeric
USER 8192
ENTRYPOINT bin/holos server

9
hack/tilt/bin/tilt Executable file
View File

@@ -0,0 +1,9 @@
#! /bin/bash
# Override kubeconfig so we can create it with local()
set -euo pipefail
TOPLEVEL="$(cd $(dirname "$0")/.. && pwd)"
export NAMESPACE="${USER}-holos"
export KUBECONFIG="${TOPLEVEL}/kubeconfig"
envsubst < "${KUBECONFIG}.template" > "${KUBECONFIG}"
export TILT_WRAPPER=1
exec tilt "$@"

View File

@@ -101,15 +101,44 @@ spec:
gateways:
- istio-ingress/default
hosts:
- '{developer}.holos.dev.k2.ois.run'
- '{developer}.app.dev.k2.holos.run'
http:
- route:
- name: "coffee-ui"
match:
- uri:
prefix: "/ui"
route:
- destination:
host: coffee
port:
number: 4200
- name: "holos-api"
route:
- destination:
host: '{name}'
port:
number: {listen_port}
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- protocol: TCP
port: 4200
---
apiVersion: v1
kind: Endpoints
metadata:
name: coffee
subsets:
- addresses:
- ip: 192.168.2.21
ports:
- port: 4200
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: holos
@@ -120,117 +149,6 @@ metadata:
imagePullSecrets:
- name: kube-system-ecr-image-pull-creds
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
labels:
app: '{name}'
holos.run/developer: '{developer}'
name: '{name}-allow-groups'
namespace: '{namespace}'
spec:
action: ALLOW
rules:
- when:
- key: request.auth.claims[groups]
values:
- holos-developer
- holos-developer@openinfrastructure.co
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-allow-nothing'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: ALLOW
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-allow-well-known-paths'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: ALLOW
rules:
- to:
- operation:
paths:
- /healthz
- /metrics
- /callbacks/github
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: '{name}-auth'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
action: CUSTOM
provider:
name: dev-holos-sso
rules:
- to:
- operation:
notPaths:
- /healthz
- /metrics
- /callbacks/github
when:
- key: request.headers[Authorization]
notValues:
- Bearer *
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: '{name}'
namespace: '{namespace}'
labels:
app: '{name}'
holos.run/developer: '{developer}'
spec:
jwtRules:
- audiences:
- https://sso.dev.holos.run
forwardOriginalToken: true
fromHeaders:
- name: x-auth-request-access-token
issuer: https://idex.core.ois.run
jwksUri: https://idex.core.ois.run/keys
- audiences:
- holos-cli
forwardOriginalToken: true
fromHeaders:
- name: authorization
prefix: 'Bearer '
issuer: https://idex.core.ois.run
jwksUri: https://idex.core.ois.run/keys
selector:
matchLabels:
holos.run/authz: dev-holos-sso
---
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PGAdmin
metadata:
@@ -266,19 +184,9 @@ spec:
databases:
- holos
options: 'SUPERUSER'
- name: kratos
databases:
- kratos
options: 'SUPERUSER'
- name: hydra
databases:
- hydra
options: 'SUPERUSER'
- name: '{developer}'
databases:
- holos
- kratos
- hydra
- '{developer}'
options: 'SUPERUSER'
# https://access.crunchydata.com/documentation/postgres-operator/latest/architecture/user-management

View File

@@ -1,10 +1,10 @@
// Package holos defines types for the rest of the system.
package holos
// A PathCueMod is a string representing the filesystem path of a cue module.
// It is given a unique type so the API is clear.
// A PathCueMod is a string representing the absolute filesystem path of a cue
// module. It is given a unique type so the API is clear.
type PathCueMod string
// A InstancePath is a string representing the filesystem path of a holos instance.
// It is given a unique type so the API is clear.
// A InstancePath is a string representing the absolute filesystem path of a
// holos instance. It is given a unique type so the API is clear.
type InstancePath string

302
internal/builder/builder.go Normal file
View File

@@ -0,0 +1,302 @@
// Package builder is responsible for building fully rendered kubernetes api
// objects from various input directories. A directory may contain a platform
// spec or a component spec.
package builder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/holos-run/holos/api/v1alpha1"
"github.com/holos-run/holos"
"github.com/holos-run/holos/internal/client"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
)
const (
KubernetesObjects = v1alpha1.KubernetesObjectsKind
// Helm is the value of the kind field of holos build output indicating helm
// values and helm command information.
Helm = v1alpha1.HelmChartKind
// Skip is the value when the instance should be skipped
Skip = "Skip"
// KustomizeBuild is the value of the kind field of cue output indicating holos should process the component using kustomize build to render output.
KustomizeBuild = v1alpha1.KustomizeBuildKind
)
// An Option configures a Builder
type Option func(*config)
type config struct {
args []string
cluster string
}
type Builder struct {
cfg config
}
// New returns a new *Builder configured by opts Option.
func New(opts ...Option) *Builder {
var cfg config
for _, f := range opts {
f(&cfg)
}
b := &Builder{cfg: cfg}
return b
}
// Entrypoints configures the leaf directories Builder builds.
func Entrypoints(args []string) Option {
return func(cfg *config) { cfg.args = args }
}
// Cluster configures the cluster name for the holos component instance.
func Cluster(name string) Option {
return func(cfg *config) { cfg.cluster = name }
}
// Cluster returns the cluster name of the component instance being built.
func (b *Builder) Cluster() string {
return b.cfg.cluster
}
// Instances returns the cue build instances being built.
func (b *Builder) Instances(ctx context.Context, cfg *client.Config) ([]*build.Instance, error) {
log := logger.FromContext(ctx)
mod, err := b.findCueMod()
if err != nil {
return nil, errors.Wrap(err)
}
dir := string(mod)
cueConfig := load.Config{Dir: dir}
// Get the platform model from the PlatformConfig
pc, err := client.LoadPlatformConfig(ctx, dir)
if err != nil {
return nil, errors.Wrap(err)
}
data, err := json.Marshal(pc)
if err != nil {
return nil, errors.Wrap(err)
}
// Refer to https://github.com/cue-lang/cue/blob/v0.7.0/cmd/cue/cmd/common.go#L429
cueConfig.Tags = append(cueConfig.Tags, "platform_config="+string(data))
if b.Cluster() != "" {
cueConfig.Tags = append(cueConfig.Tags, "cluster="+b.Cluster())
}
log.DebugContext(ctx, fmt.Sprintf("cue: tags %v", cueConfig.Tags))
prefix := []string{"cue", "export", "--out", "yaml"}
for _, tag := range cueConfig.Tags {
prefix = append(prefix, "-t", fmt.Sprintf("'%s'", tag))
}
// Make args relative to the module directory
args := make([]string, len(b.cfg.args))
for idx, path := range b.cfg.args {
target, err := filepath.Abs(path)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("could not find absolute path: %w", err))
}
relPath, err := filepath.Rel(dir, target)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("invalid argument, must be relative to cue.mod: %w", err))
}
relPath = "./" + relPath
args[idx] = relPath
equiv := make([]string, len(prefix), 1+len(prefix))
copy(equiv, prefix)
equiv = append(equiv, relPath)
log.Debug(strings.Join(equiv, " "), "comment", "cue equivalent command")
}
return load.Instances(args, &cueConfig), nil
}
func (b *Builder) Run(ctx context.Context, cfg *client.Config) (results []*v1alpha1.Result, err error) {
log := logger.FromContext(ctx)
log.DebugContext(ctx, "cue: building instances")
instances, err := b.Instances(ctx, cfg)
if err != nil {
return nil, err
}
results = make([]*v1alpha1.Result, 0, len(instances)*8)
// Each CUE instance provides a BuildPlan
for idx, instance := range instances {
log.DebugContext(ctx, "cue: building instance", "idx", idx, "dir", instance.Dir)
r, err := b.runInstance(ctx, instance)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("could not run: %w", err))
}
results = append(results, r...)
}
return results, nil
}
func (b Builder) runInstance(ctx context.Context, instance *build.Instance) (results []*v1alpha1.Result, err error) {
path := holos.InstancePath(instance.Dir)
log := logger.FromContext(ctx).With("dir", path)
if err := instance.Err; err != nil {
return nil, errors.Wrap(fmt.Errorf("could not load: %w", err))
}
cueCtx := cuecontext.New()
value := cueCtx.BuildInstance(instance)
if err := value.Err(); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not build %s: %w", instance.Dir, err))
}
log.DebugContext(ctx, "cue: validating instance")
if err := value.Validate(); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not validate: %w", err))
}
log.DebugContext(ctx, "cue: decoding holos build plan")
jsonBytes, err := value.MarshalJSON()
if err != nil {
return nil, errors.Wrap(fmt.Errorf("could not marshal cue instance %s: %w", instance.Dir, err))
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
// Discriminate the type of build plan.
tm := &v1alpha1.TypeMeta{}
err = decoder.Decode(tm)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("invalid BuildPlan: %s: %w", instance.Dir, err))
}
log.DebugContext(ctx, "cue: discriminated build kind: "+tm.Kind, "kind", tm.Kind, "apiVersion", tm.APIVersion)
// New decoder for the full object
decoder = json.NewDecoder(bytes.NewReader(jsonBytes))
// TODO: When we release v1, explicitly allow unknown fields so we can add
// fields without needing to bump the major version. Disallow until we reach
// v1 for clear error reporting.
decoder.DisallowUnknownFields()
switch tm.Kind {
// TODO(jeff) Process a v1alpha1.Result here, the result is tightly coupled to a BuildPlan.
case "BuildPlan":
var bp v1alpha1.BuildPlan
if err = decoder.Decode(&bp); err != nil {
err = errors.Wrap(fmt.Errorf("could not decode BuildPlan %s: %w", instance.Dir, err))
return
}
results, err = b.buildPlan(ctx, &bp, path)
if err != nil {
return results, err
}
default:
err = errors.Wrap(fmt.Errorf("unknown kind: %v", tm.Kind))
}
return results, err
}
func (b *Builder) buildPlan(ctx context.Context, buildPlan *v1alpha1.BuildPlan, path holos.InstancePath) (results []*v1alpha1.Result, err error) {
log := logger.FromContext(ctx)
if err := buildPlan.Validate(); err != nil {
log.WarnContext(ctx, "could not validate", "skipped", true, "err", err)
return nil, errors.Wrap(fmt.Errorf("could not validate %w", err))
}
if buildPlan.Spec.Disabled {
log.DebugContext(ctx, "skipped: spec.disabled is true", "skipped", true)
return
}
// TODO: concurrent renders
results = make([]*v1alpha1.Result, 0, buildPlan.ResultCapacity())
log.DebugContext(ctx, "allocated results slice", "cap", buildPlan.ResultCapacity())
for _, component := range buildPlan.Spec.Components.Resources {
if result, err := component.Render(ctx, path); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.KubernetesObjectsList {
if result, err := component.Render(ctx, path); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.HelmChartList {
if result, err := component.Render(ctx, path); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
for _, component := range buildPlan.Spec.Components.KustomizeBuildList {
if result, err := component.Render(ctx, path); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not render: %w", err))
} else {
results = append(results, result)
}
}
// Add a separate Result if there are DeployFiles from the BuildPlan.
if len(buildPlan.Spec.DeployFiles) > 0 {
results = append(results, &v1alpha1.Result{
HolosComponent: v1alpha1.HolosComponent{
TypeMeta: buildPlan.TypeMeta,
Metadata: buildPlan.Metadata,
},
DeployFiles: buildPlan.Spec.DeployFiles,
})
}
log.DebugContext(ctx, "returning results", "len", len(results))
return results, nil
}
// findCueMod returns the root module location containing the cue.mod file or
// directory or an error if the builder arguments do not share a common root
// module.
func (b *Builder) findCueMod() (dir holos.PathCueMod, err error) {
for _, origPath := range b.cfg.args {
absPath, err := filepath.Abs(origPath)
if err != nil {
return "", err
}
path := holos.PathCueMod(absPath)
for {
if _, err := os.Stat(filepath.Join(string(path), "cue.mod")); err == nil {
if dir != "" && dir != path {
return "", fmt.Errorf("multiple modules not supported: %v is not %v", dir, path)
}
dir = path
break
} else if !os.IsNotExist(err) {
return "", err
}
parentPath := holos.PathCueMod(filepath.Dir(string(path)))
if parentPath == path {
return "", fmt.Errorf("no cue.mod from root to leaf: %v", origPath)
}
path = parentPath
}
}
return dir, nil
}

View File

@@ -0,0 +1,90 @@
package builder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/cuecontext"
"github.com/holos-run/holos"
"github.com/holos-run/holos/api/v1alpha1"
"github.com/holos-run/holos/internal/client"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
)
// Platform builds a platform
func (b *Builder) Platform(ctx context.Context, cfg *client.Config) (*v1alpha1.Platform, error) {
log := logger.FromContext(ctx)
log.DebugContext(ctx, "cue: building platform instance")
instances, err := b.Instances(ctx, cfg)
if err != nil {
return nil, errors.Wrap(err)
}
if len(instances) != 1 {
return nil, errors.Wrap(errors.New(fmt.Sprintf("instances length %d must be exactly 1", len(instances))))
}
// We only process the first instance, assume the render platform subcommand enforces this.
instance := instances[0]
log.DebugContext(ctx, "cue: building instance", "dir", instance.Dir)
p, err := b.runPlatform(ctx, instance)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("could not build platform: %w", err))
}
return p, nil
}
func (b Builder) runPlatform(ctx context.Context, instance *build.Instance) (*v1alpha1.Platform, error) {
path := holos.InstancePath(instance.Dir)
log := logger.FromContext(ctx).With("dir", path)
if err := instance.Err; err != nil {
return nil, errors.Wrap(fmt.Errorf("could not load: %w", err))
}
cueCtx := cuecontext.New()
value := cueCtx.BuildInstance(instance)
if err := value.Err(); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not build %s: %w", instance.Dir, err))
}
log.DebugContext(ctx, "cue: validating instance")
if err := value.Validate(); err != nil {
return nil, errors.Wrap(fmt.Errorf("could not validate: %w", err))
}
log.DebugContext(ctx, "cue: decoding holos platform")
jsonBytes, err := value.MarshalJSON()
if err != nil {
return nil, errors.Wrap(fmt.Errorf("could not marshal cue instance %s: %w", instance.Dir, err))
}
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
// Discriminate the type of build plan.
tm := &v1alpha1.TypeMeta{}
err = decoder.Decode(tm)
if err != nil {
return nil, errors.Wrap(fmt.Errorf("invalid platform: %s: %w", instance.Dir, err))
}
log.DebugContext(ctx, "cue: discriminated build kind: "+tm.Kind, "kind", tm.Kind, "apiVersion", tm.APIVersion)
// New decoder for the full object
decoder = json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.DisallowUnknownFields()
var pf v1alpha1.Platform
switch tm.Kind {
case "Platform":
if err = decoder.Decode(&pf); err != nil {
err = errors.Wrap(fmt.Errorf("could not decode platform %s: %w", instance.Dir, err))
return nil, err
}
return &pf, nil
default:
err = errors.Wrap(fmt.Errorf("unknown kind: %v", tm.Kind))
}
return nil, err
}

View File

@@ -0,0 +1,57 @@
package build
import (
"fmt"
"log/slog"
"strings"
"github.com/holos-run/holos/internal/builder"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/client"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/server/middleware/logger"
"github.com/spf13/cobra"
)
// makeBuildRunFunc returns the internal implementation of the build cli command
func makeBuildRunFunc(cfg *client.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
logger.FromContext(ctx).DebugContext(ctx, "RunE", "args", args)
build := builder.New(builder.Entrypoints(args), builder.Cluster(cfg.Holos().ClusterName()))
results, err := build.Run(ctx, cfg)
if err != nil {
return err
}
outs := make([]string, 0, len(results))
for idx, result := range results {
if result == nil || result.Skip {
slog.Debug("skip result", "idx", idx, "result", result)
continue
}
slog.Debug("append result", "idx", idx, "result.kind", result.Kind)
outs = append(outs, result.AccumulatedOutput())
}
out := strings.Join(outs, "---\n")
if _, err := fmt.Fprintln(cmd.OutOrStdout(), out); err != nil {
return errors.Wrap(err)
}
return nil
}
}
// New returns the build subcommand for the root command
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("build [directory...]")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "build kubernetes api objects from a directory"
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
config := client.NewConfig(cfg)
cmd.PersistentFlags().AddGoFlagSet(config.ClientFlagSet())
cmd.PersistentFlags().AddGoFlagSet(config.TokenFlagSet())
cmd.RunE = makeBuildRunFunc(config)
return cmd
}

View File

@@ -1,10 +1,7 @@
package command
import (
"fmt"
"github.com/holos-run/holos/pkg/errors"
"github.com/holos-run/holos/pkg/version"
"github.com/holos-run/holos/version"
"github.com/spf13/cobra"
)
@@ -20,11 +17,9 @@ func New(name string) *cobra.Command {
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
RunE: func(c *cobra.Command, args []string) error {
return errors.Wrap(fmt.Errorf("could not run %v: not implemented", c.Name()))
},
SilenceUsage: true,
SilenceErrors: true,
}
cmd.Flags().SortFlags = false
return cmd
}

View File

@@ -0,0 +1,50 @@
// Package controller integrates Choria Machine Room into Holos for cluster management.
package controller
import (
"context"
"fmt"
mr "github.com/choria-io/machine-room"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/version"
"github.com/spf13/cobra"
)
var (
// SigningKey is the public key from choria jwt keys machine-signer.seed machine-signer.public, refer to gen-machine-signer.
SigningKey = "2a136e3875f4375968ae8e8d400ba24864d3ed7c4109675f357d32cc3ca1d5a7"
)
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("controller")
cmd.Args = cobra.ArbitraryArgs
cmd.DisableFlagParsing = true
cmd.RunE = func(c *cobra.Command, args []string) error {
if SigningKey == "" {
return errors.Wrap(fmt.Errorf("could not run: controller.SigningKey not set from build variables"))
}
ctx := c.Context()
if ctx == nil {
ctx = context.Background()
}
app, err := mr.New(mr.Options{
Name: "controller",
Contact: "jeff@openinfrastructure.co",
Version: version.Version,
Help: "Holos Controller",
MachineSigningKey: SigningKey,
Args: args,
})
if err != nil {
return errors.Wrap(fmt.Errorf("could not make machine room app: %w", err))
}
return app.Run(ctx)
}
return cmd
}

Some files were not shown because too many files have changed in this diff Show More