mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 00:37:45 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ddde7b49 | ||
|
|
5cced6fb51 | ||
|
|
a82ebf43b6 | ||
|
|
ebb6d6205a | ||
|
|
58950c469a | ||
|
|
0eebdaf0c7 | ||
|
|
54e2f28f4c | ||
|
|
d4d50ef12b | ||
|
|
075f2b16a4 | ||
|
|
6f8008a53c | ||
|
|
0618b52bae | ||
|
|
f1951c5db3 | ||
|
|
dad12acd8d | ||
|
|
a4503e076f | ||
|
|
09ddd339b8 | ||
|
|
bc94f4b6b8 | ||
|
|
564406f60f |
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM quay.io/holos-run/debian:bullseye AS final
|
||||
USER root
|
||||
WORKDIR /app
|
||||
ADD bin bin
|
||||
RUN chown -R app: /app
|
||||
# Kubernetes requires the user to be numeric
|
||||
USER 8192
|
||||
ENTRYPOINT bin/holos server
|
||||
7
Makefile
7
Makefile
@@ -7,7 +7,7 @@ REPO_PATH=$(ORG_PATH)/$(PROJ)
|
||||
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
|
||||
DOCKER_REPO=quay.io/holos-run/holos
|
||||
IMAGE_NAME=$(DOCKER_REPO)
|
||||
|
||||
$( shell mkdir -p bin)
|
||||
@@ -147,6 +147,11 @@ frontend: buf
|
||||
cd internal/frontend/holos && ng build
|
||||
touch internal/frontend/frontend.go
|
||||
|
||||
.PHONY: image
|
||||
image: build ## Docker image build
|
||||
docker build . -t ${DOCKER_REPO}:v$(shell ./bin/holos --version)
|
||||
docker push ${DOCKER_REPO}:v$(shell ./bin/holos --version)
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help menu.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/holos-run/holos"
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
@@ -121,6 +122,14 @@ func (hc *HelmChart) helm(ctx context.Context, r *Result, path holos.InstancePat
|
||||
}
|
||||
|
||||
// cacheChart stores a cached copy of Chart in the chart subdirectory of path.
|
||||
//
|
||||
// It is assumed that the only method responsible for writing to chartDir is
|
||||
// cacheChart itself.
|
||||
//
|
||||
// This relies on the atomicity of moving temporary directories into place on
|
||||
// the same filesystem via os.Rename. If a syscall.EEXIST error occurs during
|
||||
// renaming, it indicates that the cached chart already exists, which is an
|
||||
// expected scenario when this function is called concurrently.
|
||||
func cacheChart(ctx context.Context, path holos.InstancePath, chartDir string, chart Chart) error {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
@@ -156,11 +165,16 @@ func cacheChart(ctx context.Context, path holos.InstancePath, chartDir string, c
|
||||
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))
|
||||
var linkErr *os.LinkError
|
||||
if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EEXIST) {
|
||||
log.DebugContext(ctx, "cache already exists", "chart", chart.Name, "chart_version", chart.Version, "path", cachePath)
|
||||
} else {
|
||||
return errors.Wrap(fmt.Errorf("could not rename: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "cached", "chart", chart.Name, "version", chart.Version, "path", cachePath)
|
||||
log.InfoContext(ctx, "cached", "chart", chart.Name, "chart_version", chart.Version, "path", cachePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@ process. This ensures a namespace scoped `SecretStore` is created to sync
|
||||
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.
|
||||
6. Get a timestamp: `STAMP="$(date +%s)"`
|
||||
7. Run the job to populate ecr creds: `kubectl create job -n holos-system --from=cronjob/ecr-creds-manager ecr-creds-manager-$STAMP`
|
||||
8. Wait for the job to complete: `kubectl -n holos-system logs -l job-name=ecr-creds-manager-$STAMP -f`
|
||||
9. Apply the `namespaces` component to the workload clusters
|
||||
10. 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-${STAMP}`
|
||||
11. Wait for the job to complete: `kubectl -n holos-system logs -l job-name=eso-creds-refresher-${STAMP}`
|
||||
12. Apply the secretstores component to the workload cluster.
|
||||
13. Apply any other cluster specific components which were modified by the `holos render platform ./platform` command.
|
||||
|
||||
Your namespace is created and you have the ability to create secrets in the management cluster and pull them using ExternalSecret resources. (edited)
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ func NewPlatform(cfg *client.Config) *cobra.Command {
|
||||
}
|
||||
|
||||
func NewPlatformConfig(cfg *client.Config) *cobra.Command {
|
||||
cmd := command.New("config")
|
||||
cmd.Short = "pull platform config"
|
||||
cmd := command.New("model")
|
||||
cmd.Aliases = []string{"config"}
|
||||
cmd.Short = "pull platform model"
|
||||
cmd.Args = cobra.MinimumNArgs(1)
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewPlatform(cfg *client.Config) *cobra.Command {
|
||||
cmd.Args = cobra.NoArgs
|
||||
|
||||
cmd.AddCommand(NewPlatformForm(cfg))
|
||||
// cmd.AddCommand(NewPlatformModel(cfg))
|
||||
cmd.AddCommand(NewPlatformModel(cfg))
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -74,3 +74,34 @@ func NewPlatformForm(cfg *client.Config) *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewPlatformModel(cfg *client.Config) *cobra.Command {
|
||||
cmd := command.New("model")
|
||||
cmd.Short = "push platform model to holos server"
|
||||
cmd.Args = cobra.MinimumNArgs(1)
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Root().Context()
|
||||
if ctx == nil {
|
||||
return errors.Wrap(errors.New("cannot execute: no context"))
|
||||
}
|
||||
ctx = logger.NewContext(ctx, logger.FromContext(ctx).With("server", cfg.Client().Server()))
|
||||
rpc := client.New(cfg)
|
||||
for _, name := range args {
|
||||
// Get the platform config for the platform id.
|
||||
p, err := client.LoadPlatformConfig(ctx, name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// Make the rpc call to update the platform form.
|
||||
if err := rpc.UpdatePlatformModel(ctx, p.PlatformId, p.PlatformModel); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
slog.Default().InfoContext(ctx, fmt.Sprintf("pushed: %s/ui/platform/%s", cfg.Client().Server(), p.PlatformId))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/holos-run/holos/internal/builder"
|
||||
"github.com/holos-run/holos/internal/cli/command"
|
||||
@@ -113,6 +114,9 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
|
||||
cmd.PersistentFlags().AddGoFlagSet(config.ClientFlagSet())
|
||||
cmd.PersistentFlags().AddGoFlagSet(config.TokenFlagSet())
|
||||
|
||||
var concurrency int
|
||||
cmd.Flags().IntVar(&concurrency, "concurrency", min(runtime.NumCPU(), 8), "Number of concurrent components to render")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Root().Context()
|
||||
build := builder.New(builder.Entrypoints(args))
|
||||
@@ -122,7 +126,7 @@ func NewPlatform(cfg *holos.Config) *cobra.Command {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
return render.Platform(ctx, platform, cmd.ErrOrStderr())
|
||||
return render.Platform(ctx, concurrency, platform, cmd.ErrOrStderr())
|
||||
}
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -91,6 +91,22 @@ func (c *Client) UpdateForm(ctx context.Context, platformID string, form *object
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdatePlatformModel(ctx context.Context, platformID string, model *structpb.Struct) error {
|
||||
start := time.Now()
|
||||
req := &platform.UpdatePlatformRequest{
|
||||
PlatformId: platformID,
|
||||
Update: &platform.PlatformMutation{Model: model},
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"model"}},
|
||||
}
|
||||
_, err := c.pltSvc.UpdatePlatform(ctx, connect.NewRequest(req))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
log := logger.FromContext(ctx)
|
||||
log.DebugContext(ctx, "updated platform", "platform_id", platformID, "duration", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlatformModel gets the platform model from the PlatformService.
|
||||
func (c *Client) PlatformModel(ctx context.Context, platformID string) (*structpb.Struct, error) {
|
||||
start := time.Now()
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
// Code generated by timoni. DO NOT EDIT.
|
||||
|
||||
//timoni:generate timoni vendor crd -f https://raw.githubusercontent.com/crossplane-contrib/provider-upjet-aws/v1.5.0/package/crds/aws.upbound.io_providerconfigs.yaml
|
||||
|
||||
package v1beta1
|
||||
|
||||
import "strings"
|
||||
|
||||
// A ProviderConfig configures the AWS provider.
|
||||
#ProviderConfig: {
|
||||
// APIVersion defines the versioned schema of this representation
|
||||
// of an object.
|
||||
// Servers should convert recognized schemas to the latest
|
||||
// internal value, and
|
||||
// may reject unrecognized values.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
apiVersion: "aws.upbound.io/v1beta1"
|
||||
|
||||
// Kind is a string value representing the REST resource this
|
||||
// object represents.
|
||||
// Servers may infer this from the endpoint the client submits
|
||||
// requests to.
|
||||
// Cannot be updated.
|
||||
// In CamelCase.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
kind: "ProviderConfig"
|
||||
metadata!: {
|
||||
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
namespace?: strings.MaxRunes(63) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
labels?: {
|
||||
[string]: string
|
||||
}
|
||||
annotations?: {
|
||||
[string]: string
|
||||
}
|
||||
}
|
||||
|
||||
// A ProviderConfigSpec defines the desired state of a
|
||||
// ProviderConfig.
|
||||
spec!: #ProviderConfigSpec
|
||||
}
|
||||
|
||||
// A ProviderConfigSpec defines the desired state of a
|
||||
// ProviderConfig.
|
||||
#ProviderConfigSpec: {
|
||||
// AssumeRoleChain defines the options for assuming an IAM role
|
||||
assumeRoleChain?: [...{
|
||||
// ExternalID is the external ID used when assuming role.
|
||||
externalID?: string
|
||||
|
||||
// AssumeRoleARN to assume with provider credentials
|
||||
roleARN?: string
|
||||
|
||||
// Tags is list of session tags that you want to pass. Each
|
||||
// session tag consists of a key
|
||||
// name and an associated value. For more information about
|
||||
// session tags, see
|
||||
// Tagging STS Sessions
|
||||
// (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html).
|
||||
tags?: [...{
|
||||
// Name of the tag.
|
||||
// Key is a required field
|
||||
key: string
|
||||
|
||||
// Value of the tag.
|
||||
// Value is a required field
|
||||
value: string
|
||||
}]
|
||||
|
||||
// TransitiveTagKeys is a list of keys for session tags that you
|
||||
// want to set as transitive. If you set a
|
||||
// tag key as transitive, the corresponding key and value passes
|
||||
// to subsequent
|
||||
// sessions in a role chain. For more information, see Chaining
|
||||
// Roles with Session Tags
|
||||
// (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining).
|
||||
transitiveTagKeys?: [...string]
|
||||
}]
|
||||
|
||||
// Credentials required to authenticate to this provider.
|
||||
credentials: {
|
||||
env?: {
|
||||
// Name is the name of an environment variable.
|
||||
name: string
|
||||
}
|
||||
fs?: {
|
||||
// Path is a filesystem path.
|
||||
path: string
|
||||
}
|
||||
|
||||
// A SecretRef is a reference to a secret key that contains the
|
||||
// credentials
|
||||
// that must be used to connect to the provider.
|
||||
secretRef?: {
|
||||
// The key to select.
|
||||
key: string
|
||||
|
||||
// Name of the secret.
|
||||
name: string
|
||||
|
||||
// Namespace of the secret.
|
||||
namespace: string
|
||||
}
|
||||
|
||||
// Source of the provider credentials.
|
||||
source: "None" | "Secret" | "IRSA" | "WebIdentity" | "Upbound"
|
||||
upbound?: {
|
||||
// WebIdentity defines the options for assuming an IAM role with a
|
||||
// Web
|
||||
// Identity.
|
||||
webIdentity?: {
|
||||
// AssumeRoleARN to assume with provider credentials
|
||||
roleARN?: string
|
||||
|
||||
// RoleSessionName is the session name, if you wish to uniquely
|
||||
// identify this session.
|
||||
roleSessionName?: string
|
||||
|
||||
// TokenConfig is the Web Identity Token config to assume the
|
||||
// role.
|
||||
tokenConfig?: {
|
||||
fs?: {
|
||||
// Path is a filesystem path.
|
||||
path: string
|
||||
}
|
||||
|
||||
// A SecretRef is a reference to a secret key that contains the
|
||||
// credentials
|
||||
// that must be used to obtain the web identity token.
|
||||
secretRef?: {
|
||||
// The key to select.
|
||||
key: string
|
||||
|
||||
// Name of the secret.
|
||||
name: string
|
||||
|
||||
// Namespace of the secret.
|
||||
namespace: string
|
||||
}
|
||||
|
||||
// Source is the source of the web identity token.
|
||||
source: "Secret" | "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebIdentity defines the options for assuming an IAM role with a
|
||||
// Web Identity.
|
||||
webIdentity?: {
|
||||
// AssumeRoleARN to assume with provider credentials
|
||||
roleARN?: string
|
||||
|
||||
// RoleSessionName is the session name, if you wish to uniquely
|
||||
// identify this session.
|
||||
roleSessionName?: string
|
||||
|
||||
// TokenConfig is the Web Identity Token config to assume the
|
||||
// role.
|
||||
tokenConfig?: {
|
||||
fs?: {
|
||||
// Path is a filesystem path.
|
||||
path: string
|
||||
}
|
||||
|
||||
// A SecretRef is a reference to a secret key that contains the
|
||||
// credentials
|
||||
// that must be used to obtain the web identity token.
|
||||
secretRef?: {
|
||||
// The key to select.
|
||||
key: string
|
||||
|
||||
// Name of the secret.
|
||||
name: string
|
||||
|
||||
// Namespace of the secret.
|
||||
namespace: string
|
||||
}
|
||||
|
||||
// Source is the source of the web identity token.
|
||||
source: "Secret" | "Filesystem"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint is where you can override the default endpoint
|
||||
// configuration
|
||||
// of AWS calls made by the provider.
|
||||
endpoint?: {
|
||||
// Specifies if the endpoint's hostname can be modified by the
|
||||
// SDK's API
|
||||
// client.
|
||||
//
|
||||
//
|
||||
// If the hostname is mutable the SDK API clients may modify any
|
||||
// part of
|
||||
// the hostname based on the requirements of the API, (e.g.
|
||||
// adding, or
|
||||
// removing content in the hostname). Such as, Amazon S3 API
|
||||
// client
|
||||
// prefixing "bucketname" to the hostname, or changing the
|
||||
// hostname service name component from "s3." to
|
||||
// "s3-accesspoint.dualstack."
|
||||
// for the dualstack endpoint of an S3 Accesspoint resource.
|
||||
//
|
||||
//
|
||||
// Care should be taken when providing a custom endpoint for an
|
||||
// API. If the
|
||||
// endpoint hostname is mutable, and the client cannot modify the
|
||||
// endpoint
|
||||
// correctly, the operation call will most likely fail, or have
|
||||
// undefined
|
||||
// behavior.
|
||||
//
|
||||
//
|
||||
// If hostname is immutable, the SDK API clients will not modify
|
||||
// the
|
||||
// hostname of the URL. This may cause the API client not to
|
||||
// function
|
||||
// correctly if the API requires the operation specific hostname
|
||||
// values
|
||||
// to be used by the client.
|
||||
//
|
||||
//
|
||||
// This flag does not modify the API client's behavior if this
|
||||
// endpoint
|
||||
// will be used instead of Endpoint Discovery, or if the endpoint
|
||||
// will be
|
||||
// used to perform Endpoint Discovery. That behavior is configured
|
||||
// via the
|
||||
// API Client's Options.
|
||||
// Note that this is effective only for resources that use AWS SDK
|
||||
// v2.
|
||||
hostnameImmutable?: bool
|
||||
|
||||
// The AWS partition the endpoint belongs to.
|
||||
partitionId?: string
|
||||
|
||||
// Specifies the list of services you want endpoint to be used for
|
||||
services?: [...string]
|
||||
|
||||
// The signing method that should be used for signing the requests
|
||||
// to the
|
||||
// endpoint.
|
||||
signingMethod?: string
|
||||
|
||||
// The service name that should be used for signing the requests
|
||||
// to the
|
||||
// endpoint.
|
||||
signingName?: string
|
||||
|
||||
// The region that should be used for signing the request to the
|
||||
// endpoint.
|
||||
// For IAM, which doesn't have any region, us-east-1 is used to
|
||||
// sign the
|
||||
// requests, which is the only signing region of IAM.
|
||||
signingRegion?: string
|
||||
|
||||
// The source of the Endpoint. By default, this will be
|
||||
// ServiceMetadata.
|
||||
// When providing a custom endpoint, you should set the source as
|
||||
// Custom.
|
||||
// If source is not provided when providing a custom endpoint, the
|
||||
// SDK may not
|
||||
// perform required host mutations correctly. Source should be
|
||||
// used along with
|
||||
// HostnameImmutable property as per the usage requirement.
|
||||
// Note that this is effective only for resources that use AWS SDK
|
||||
// v2.
|
||||
source?: "ServiceMetadata" | "Custom"
|
||||
|
||||
// URL lets you configure the endpoint URL to be used in SDK
|
||||
// calls.
|
||||
url: {
|
||||
// Dynamic lets you configure the behavior of endpoint URL
|
||||
// resolver.
|
||||
dynamic?: {
|
||||
// Host is the address of the main host that the resolver will use
|
||||
// to
|
||||
// prepend protocol, service and region configurations.
|
||||
// For example, the final URL for EC2 in us-east-1 looks like
|
||||
// https://ec2.us-east-1.amazonaws.com
|
||||
// You would need to use "amazonaws.com" as Host and "https" as
|
||||
// protocol
|
||||
// to have the resolver construct it.
|
||||
host: string
|
||||
|
||||
// Protocol is the HTTP protocol that will be used in the URL.
|
||||
// Currently,
|
||||
// only http and https are supported.
|
||||
protocol: "http" | "https"
|
||||
}
|
||||
|
||||
// Static is the full URL you'd like the AWS SDK to use.
|
||||
// Recommended for using tools like localstack where a single host
|
||||
// is exposed
|
||||
// for all services and regions.
|
||||
static?: string
|
||||
|
||||
// You can provide a static URL that will be used regardless of
|
||||
// the service
|
||||
// and region by choosing Static type. Alternatively, you can
|
||||
// provide
|
||||
// configuration for dynamically resolving the URL with the config
|
||||
// you provide
|
||||
// once you set the type as Dynamic.
|
||||
type: "Static" | "Dynamic"
|
||||
}
|
||||
}
|
||||
|
||||
// Whether to enable the request to use path-style addressing,
|
||||
// i.e., https://s3.amazonaws.com/BUCKET/KEY.
|
||||
s3_use_path_style?: bool
|
||||
|
||||
// Whether to skip credentials validation via the STS API.
|
||||
// This can be useful for testing and for AWS API implementations
|
||||
// that do not have STS available.
|
||||
skip_credentials_validation?: bool
|
||||
|
||||
// Whether to skip the AWS Metadata API check
|
||||
// Useful for AWS API implementations that do not have a metadata
|
||||
// API endpoint.
|
||||
skip_metadata_api_check?: bool
|
||||
|
||||
// Whether to skip validation of provided region name.
|
||||
// Useful for AWS-like implementations that use their own region
|
||||
// names or to bypass the validation for
|
||||
// regions that aren't publicly available yet.
|
||||
skip_region_validation?: bool
|
||||
|
||||
// Whether to skip requesting the account ID.
|
||||
// Useful for AWS API implementations that do not have the IAM,
|
||||
// STS API, or metadata API
|
||||
skip_requesting_account_id?: bool
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
// Code generated by timoni. DO NOT EDIT.
|
||||
|
||||
//timoni:generate timoni vendor crd -f https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/pkg.crossplane.io_functions.yaml
|
||||
|
||||
package v1beta1
|
||||
|
||||
import "strings"
|
||||
|
||||
// A Function installs an OCI compatible Crossplane package,
|
||||
// extending
|
||||
// Crossplane with support for a new kind of composition function.
|
||||
//
|
||||
//
|
||||
// Read the Crossplane documentation for
|
||||
// [more information about
|
||||
// Functions](https://docs.crossplane.io/latest/concepts/composition-functions).
|
||||
#Function: {
|
||||
// APIVersion defines the versioned schema of this representation
|
||||
// of an object.
|
||||
// Servers should convert recognized schemas to the latest
|
||||
// internal value, and
|
||||
// may reject unrecognized values.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
apiVersion: "pkg.crossplane.io/v1beta1"
|
||||
|
||||
// Kind is a string value representing the REST resource this
|
||||
// object represents.
|
||||
// Servers may infer this from the endpoint the client submits
|
||||
// requests to.
|
||||
// Cannot be updated.
|
||||
// In CamelCase.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
kind: "Function"
|
||||
metadata!: {
|
||||
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
namespace?: strings.MaxRunes(63) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
labels?: {
|
||||
[string]: string
|
||||
}
|
||||
annotations?: {
|
||||
[string]: string
|
||||
}
|
||||
}
|
||||
|
||||
// FunctionSpec specifies the configuration of a Function.
|
||||
spec!: #FunctionSpec
|
||||
}
|
||||
|
||||
// FunctionSpec specifies the configuration of a Function.
|
||||
#FunctionSpec: {
|
||||
// Map of string keys and values that can be used to organize and
|
||||
// categorize
|
||||
// (scope and select) objects. May match selectors of replication
|
||||
// controllers
|
||||
// and services.
|
||||
// More info:
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
commonLabels?: {
|
||||
[string]: string
|
||||
}
|
||||
controllerConfigRef?: {
|
||||
// Name of the ControllerConfig.
|
||||
name: string
|
||||
}
|
||||
|
||||
// IgnoreCrossplaneConstraints indicates to the package manager
|
||||
// whether to
|
||||
// honor Crossplane version constrains specified by the package.
|
||||
// Default is false.
|
||||
ignoreCrossplaneConstraints?: bool | *false
|
||||
|
||||
// Package is the name of the package that is being requested.
|
||||
package: string
|
||||
|
||||
// PackagePullPolicy defines the pull policy for the package.
|
||||
// Default is IfNotPresent.
|
||||
packagePullPolicy?: string | *"IfNotPresent"
|
||||
|
||||
// PackagePullSecrets are named secrets in the same namespace that
|
||||
// can be used
|
||||
// to fetch packages from private registries.
|
||||
packagePullSecrets?: [...{
|
||||
// Name of the referent.
|
||||
// More info:
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
// TODO: Add other useful fields. apiVersion, kind, uid?
|
||||
name?: string
|
||||
}]
|
||||
|
||||
// RevisionActivationPolicy specifies how the package controller
|
||||
// should
|
||||
// update from one revision to the next. Options are Automatic or
|
||||
// Manual.
|
||||
// Default is Automatic.
|
||||
revisionActivationPolicy?: string | *"Automatic"
|
||||
|
||||
// RevisionHistoryLimit dictates how the package controller cleans
|
||||
// up old
|
||||
// inactive package revisions.
|
||||
// Defaults to 1. Can be disabled by explicitly setting to 0.
|
||||
revisionHistoryLimit?: int | *1
|
||||
|
||||
// RuntimeConfigRef references a RuntimeConfig resource that will
|
||||
// be used
|
||||
// to configure the package runtime.
|
||||
runtimeConfigRef?: {
|
||||
// API version of the referent.
|
||||
apiVersion?: string | *"pkg.crossplane.io/v1beta1"
|
||||
|
||||
// Kind of the referent.
|
||||
kind?: string | *"DeploymentRuntimeConfig"
|
||||
|
||||
// Name of the RuntimeConfig.
|
||||
name: string
|
||||
} | *{
|
||||
name: "default"
|
||||
}
|
||||
|
||||
// SkipDependencyResolution indicates to the package manager
|
||||
// whether to skip
|
||||
// resolving dependencies for a package. Setting this value to
|
||||
// true may have
|
||||
// unintended consequences.
|
||||
// Default is false.
|
||||
skipDependencyResolution?: bool | *false
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Code generated by timoni. DO NOT EDIT.
|
||||
|
||||
//timoni:generate timoni vendor crd -f https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/pkg.crossplane.io_providers.yaml
|
||||
|
||||
package v1
|
||||
|
||||
import "strings"
|
||||
|
||||
// A Provider installs an OCI compatible Crossplane package,
|
||||
// extending
|
||||
// Crossplane with support for new kinds of managed resources.
|
||||
//
|
||||
//
|
||||
// Read the Crossplane documentation for
|
||||
// [more information about
|
||||
// Providers](https://docs.crossplane.io/latest/concepts/providers).
|
||||
#Provider: {
|
||||
// APIVersion defines the versioned schema of this representation
|
||||
// of an object.
|
||||
// Servers should convert recognized schemas to the latest
|
||||
// internal value, and
|
||||
// may reject unrecognized values.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
apiVersion: "pkg.crossplane.io/v1"
|
||||
|
||||
// Kind is a string value representing the REST resource this
|
||||
// object represents.
|
||||
// Servers may infer this from the endpoint the client submits
|
||||
// requests to.
|
||||
// Cannot be updated.
|
||||
// In CamelCase.
|
||||
// More info:
|
||||
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
kind: "Provider"
|
||||
metadata!: {
|
||||
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
namespace?: strings.MaxRunes(63) & strings.MinRunes(1) & {
|
||||
string
|
||||
}
|
||||
labels?: {
|
||||
[string]: string
|
||||
}
|
||||
annotations?: {
|
||||
[string]: string
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderSpec specifies details about a request to install a
|
||||
// provider to
|
||||
// Crossplane.
|
||||
spec!: #ProviderSpec
|
||||
}
|
||||
|
||||
// ProviderSpec specifies details about a request to install a
|
||||
// provider to
|
||||
// Crossplane.
|
||||
#ProviderSpec: {
|
||||
// Map of string keys and values that can be used to organize and
|
||||
// categorize
|
||||
// (scope and select) objects. May match selectors of replication
|
||||
// controllers
|
||||
// and services.
|
||||
// More info:
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
commonLabels?: {
|
||||
[string]: string
|
||||
}
|
||||
controllerConfigRef?: {
|
||||
// Name of the ControllerConfig.
|
||||
name: string
|
||||
}
|
||||
|
||||
// IgnoreCrossplaneConstraints indicates to the package manager
|
||||
// whether to
|
||||
// honor Crossplane version constrains specified by the package.
|
||||
// Default is false.
|
||||
ignoreCrossplaneConstraints?: bool | *false
|
||||
|
||||
// Package is the name of the package that is being requested.
|
||||
package: string
|
||||
|
||||
// PackagePullPolicy defines the pull policy for the package.
|
||||
// Default is IfNotPresent.
|
||||
packagePullPolicy?: string | *"IfNotPresent"
|
||||
|
||||
// PackagePullSecrets are named secrets in the same namespace that
|
||||
// can be used
|
||||
// to fetch packages from private registries.
|
||||
packagePullSecrets?: [...{
|
||||
// Name of the referent.
|
||||
// More info:
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
// TODO: Add other useful fields. apiVersion, kind, uid?
|
||||
name?: string
|
||||
}]
|
||||
|
||||
// RevisionActivationPolicy specifies how the package controller
|
||||
// should
|
||||
// update from one revision to the next. Options are Automatic or
|
||||
// Manual.
|
||||
// Default is Automatic.
|
||||
revisionActivationPolicy?: string | *"Automatic"
|
||||
|
||||
// RevisionHistoryLimit dictates how the package controller cleans
|
||||
// up old
|
||||
// inactive package revisions.
|
||||
// Defaults to 1. Can be disabled by explicitly setting to 0.
|
||||
revisionHistoryLimit?: int | *1
|
||||
|
||||
// RuntimeConfigRef references a RuntimeConfig resource that will
|
||||
// be used
|
||||
// to configure the package runtime.
|
||||
runtimeConfigRef?: {
|
||||
// API version of the referent.
|
||||
apiVersion?: string | *"pkg.crossplane.io/v1beta1"
|
||||
|
||||
// Kind of the referent.
|
||||
kind?: string | *"DeploymentRuntimeConfig"
|
||||
|
||||
// Name of the RuntimeConfig.
|
||||
name: string
|
||||
} | *{
|
||||
name: "default"
|
||||
}
|
||||
|
||||
// SkipDependencyResolution indicates to the package manager
|
||||
// whether to skip
|
||||
// resolving dependencies for a package. Setting this value to
|
||||
// true may have
|
||||
// unintended consequences.
|
||||
// Default is false.
|
||||
skipDependencyResolution?: bool | *false
|
||||
}
|
||||
@@ -32,9 +32,7 @@ import "strings"
|
||||
// Configuration for access control on workloads. See more details
|
||||
// at:
|
||||
// https://istio.io/docs/reference/config/security/authorization-policy.html
|
||||
#AuthorizationPolicySpec: ({} | {
|
||||
provider: _
|
||||
}) & {
|
||||
#AuthorizationPolicySpec: {
|
||||
// Optional.
|
||||
//
|
||||
// Valid Options: ALLOW, DENY, AUDIT, CUSTOM
|
||||
|
||||
67
internal/generate/platforms/holos/apps/appinfo.cue
Normal file
67
internal/generate/platforms/holos/apps/appinfo.cue
Normal file
@@ -0,0 +1,67 @@
|
||||
package holos
|
||||
|
||||
import "strings"
|
||||
|
||||
// #AppInfo represents the data structure for an application deployed onto the
|
||||
// platform. This definition constraints the sechema defined at the root.
|
||||
_AppInfo: #AppInfo & {
|
||||
metadata: name: string
|
||||
metadata: namespace: "\(spec.env)-\(metadata.name)"
|
||||
|
||||
spec: env: string
|
||||
spec: region: hostname: spec.dns.segments._region
|
||||
spec: global: hostname: spec.dns.segments._global
|
||||
|
||||
spec: dns: segments: {
|
||||
env: [] | *[spec.env]
|
||||
name: [] | [string | *metadata.name]
|
||||
cluster: [] | *[_ClusterName]
|
||||
domain: [] | *[_Platform.Model.org.domain]
|
||||
_region: strings.Join(env+name+cluster+domain, ".")
|
||||
_global: strings.Join(env+name+domain, ".")
|
||||
}
|
||||
}
|
||||
|
||||
// #AppRoute represents the HTTPRoute resources in the namespace of the Gateway.
|
||||
#AppRoute: {
|
||||
AppInfo: #AppInfo
|
||||
|
||||
Resources: {
|
||||
HTTPRoute: (HTTPRouteApp & {Hostname: AppInfo.spec.region.hostname}).HTTPRoute
|
||||
HTTPRoute: (HTTPRouteApp & {Hostname: AppInfo.spec.global.hostname}).HTTPRoute
|
||||
}
|
||||
|
||||
let HTTPRouteApp = {
|
||||
Hostname: string
|
||||
|
||||
HTTPRoute: (Hostname): {
|
||||
metadata: namespace: #IstioGatewaysNamespace
|
||||
metadata: labels: AppInfo.metadata.labels
|
||||
metadata: annotations: AppInfo.metadata.annotations
|
||||
spec: hostnames: [Hostname]
|
||||
spec: parentRefs: [{
|
||||
name: "default"
|
||||
namespace: #IstioGatewaysNamespace
|
||||
}]
|
||||
spec: rules: [
|
||||
{
|
||||
matches: [{path: {type: "PathPrefix", value: "/"}}]
|
||||
backendRefs: [{
|
||||
name: AppInfo.metadata.name
|
||||
namespace: AppInfo.metadata.namespace
|
||||
port: AppInfo.spec.port
|
||||
}]
|
||||
},
|
||||
{
|
||||
// match the authproxy path prefix
|
||||
matches: [{path: {type: "PathPrefix", value: _AuthProxy.pathPrefix}}]
|
||||
backendRefs: [{
|
||||
name: _AuthProxy.metadata.name
|
||||
namespace: _AuthProxy.metadata.namespace
|
||||
port: _AuthProxy.servicePort
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
3
internal/generate/platforms/holos/apps/dev/dev.cue
Normal file
3
internal/generate/platforms/holos/apps/dev/dev.cue
Normal file
@@ -0,0 +1,3 @@
|
||||
package holos
|
||||
|
||||
_AppInfo: #AppInfo & {spec: env: "dev"}
|
||||
@@ -0,0 +1,108 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let Image = "quay.io/holos-run/holos:v0.83.1-7-gd9fe32b"
|
||||
|
||||
_AppInfo: spec: component: "app"
|
||||
|
||||
let Objects = {
|
||||
Name: _AppInfo.status.component
|
||||
Namespace: _AppInfo.metadata.namespace
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
let Metadata = _AppInfo.metadata
|
||||
|
||||
Resources: {
|
||||
let MatchLabels = {"app.kubernetes.io/component": "server"}
|
||||
|
||||
// Grant the Gateway ns the ability to refer to the Service from HTTPRoutes.
|
||||
ReferenceGrant: (#IstioGatewaysNamespace): #ReferenceGrant
|
||||
|
||||
Service: holos: {
|
||||
apiVersion: "v1"
|
||||
metadata: Metadata
|
||||
spec: {
|
||||
type: "ClusterIP"
|
||||
selector: MatchLabels
|
||||
ports: [{
|
||||
appProtocol: "http2"
|
||||
name: "http"
|
||||
port: _AppInfo.spec.port
|
||||
protocol: "TCP"
|
||||
targetPort: _AppInfo.spec.port
|
||||
}, {
|
||||
appProtocol: "http"
|
||||
name: "metrics"
|
||||
port: 9090
|
||||
protocol: "TCP"
|
||||
targetPort: 9090
|
||||
}]
|
||||
}
|
||||
}
|
||||
Deployment: holos: {
|
||||
metadata: Metadata
|
||||
metadata: labels: MatchLabels
|
||||
spec: {
|
||||
selector: matchLabels: MatchLabels
|
||||
|
||||
template: metadata: labels: Metadata.labels
|
||||
template: metadata: labels: MatchLabels
|
||||
template: metadata: labels: "sidecar.istio.io/inject": "true"
|
||||
|
||||
strategy: rollingUpdate: maxSurge: 1
|
||||
strategy: rollingUpdate: maxUnavailable: 0
|
||||
template: {
|
||||
spec: {
|
||||
serviceAccountName: Metadata.name
|
||||
securityContext: seccompProfile: type: "RuntimeDefault"
|
||||
containers: [
|
||||
{
|
||||
name: Metadata.name
|
||||
image: Image
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
command: [
|
||||
"/app/bin/holos",
|
||||
"server",
|
||||
"--log-format=json",
|
||||
"--oidc-issuer=\(_AuthProxy.issuerURL)",
|
||||
"--oidc-audience=\(_AuthProxy.projectID)",
|
||||
]
|
||||
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.5"
|
||||
memory: "512Mi"
|
||||
}
|
||||
resources: requests: resources.limits
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
internal/generate/platforms/holos/apps/dev/holos/holos.cue
Normal file
19
internal/generate/platforms/holos/apps/dev/holos/holos.cue
Normal file
@@ -0,0 +1,19 @@
|
||||
package holos
|
||||
|
||||
_AppInfo: #AppInfo & {
|
||||
metadata: name: "holos"
|
||||
metadata: labels: {
|
||||
"app.holos.run/environment": spec.env
|
||||
"app.holos.run/name": metadata.name
|
||||
"app.holos.run/component": spec.component
|
||||
"render.holos.run/component": status.component
|
||||
}
|
||||
|
||||
spec: env: string
|
||||
spec: port: 3000
|
||||
spec: component: "app" | "infra" | "routes"
|
||||
|
||||
spec: dns: segments: name: ["app"]
|
||||
|
||||
status: component: spec.env + "-" + metadata.name + "-" + spec.component
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
_AppInfo: spec: component: "infra"
|
||||
|
||||
let Objects = {
|
||||
Name: _AppInfo.status.component
|
||||
Namespace: _AppInfo.metadata.namespace
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
let AWS_ACCOUNT = _Platform.Model.aws.accountNumber
|
||||
let Metadata = _AppInfo.metadata
|
||||
|
||||
Resources: {
|
||||
ServiceAccount: holos: {
|
||||
metadata: Metadata
|
||||
// TODO(jeff): The ecr-creds-refresher name should be refactored to a root
|
||||
// level private var so we can update it in one place.
|
||||
// Refer to ecr-creds-refresher.cue
|
||||
imagePullSecrets: [{name: "ecr-creds-\(AWS_ACCOUNT)"}]
|
||||
}
|
||||
|
||||
PostgresCluster: holos: {
|
||||
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
|
||||
metadata: name: "holos"
|
||||
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": metadata.name
|
||||
topologyKey: "topology.kubernetes.io/zone"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
_AppInfo: spec: component: "routes"
|
||||
|
||||
let Objects = {
|
||||
Name: _AppInfo.status.component
|
||||
Namespace: #IstioGatewaysNamespace
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
// HTTPRoute resources
|
||||
Resources: (#AppRoute & {AppInfo: _AppInfo}).Resources
|
||||
}
|
||||
@@ -28,6 +28,11 @@ import (
|
||||
pc "postgres-operator.crunchydata.com/postgrescluster/v1beta1"
|
||||
|
||||
app "argoproj.io/application/v1alpha1"
|
||||
|
||||
cpv1 "pkg.crossplane.io/provider/v1"
|
||||
cpdrcv1beta1 "pkg.crossplane.io/deploymentruntimeconfig/v1beta1"
|
||||
cpfuncv1beta1 "pkg.crossplane.io/function/v1beta1"
|
||||
cpawspcv1beta1 "aws.upbound.io/providerconfig/v1beta1"
|
||||
)
|
||||
|
||||
// #Resources represents kubernetes api objects output along side a build plan.
|
||||
@@ -63,6 +68,12 @@ import (
|
||||
Gateway: [string]: gwv1.#Gateway & {
|
||||
spec: gatewayClassName: string | *"istio"
|
||||
}
|
||||
|
||||
// Crossplane resources
|
||||
DeploymentRuntimeConfig: [string]: cpdrcv1beta1.#DeploymentRuntimeConfig
|
||||
Provider: [string]: cpv1.#Provider
|
||||
Function: [string]: cpfuncv1beta1.#Function
|
||||
ProviderConfig: [string]: cpawspcv1beta1.#ProviderConfig
|
||||
}
|
||||
|
||||
#ReferenceGrant: rgv1.#ReferenceGrant & {
|
||||
|
||||
@@ -60,6 +60,7 @@ let Chart = {
|
||||
|
||||
// Refer to https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/
|
||||
let Policy = [
|
||||
"g, argocd-view, role:readonly",
|
||||
"g, prod-cluster-view, role:readonly",
|
||||
"g, prod-cluster-edit, role:readonly",
|
||||
"g, prod-cluster-admin, role:admin",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package holos
|
||||
|
||||
// _DBName is the database name used across multiple holos components in this project
|
||||
_DBName: "backstage"
|
||||
|
||||
_Component: {
|
||||
metadata: name: "backstage"
|
||||
metadata: namespace: "backstage"
|
||||
spec: hostname: "backstage.admin.\(_ClusterName).\(_Platform.Model.org.domain)"
|
||||
spec: port: 7007
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package holos
|
||||
|
||||
import (
|
||||
is "cert-manager.io/issuer/v1"
|
||||
crt "cert-manager.io/certificate/v1"
|
||||
)
|
||||
|
||||
// Manage an Issuer for the database.
|
||||
|
||||
// Both cockroach and postgres handle tls database connections with cert manager
|
||||
// PGO: https://github.com/CrunchyData/postgres-operator-examples/tree/main/kustomize/certmanager/certman
|
||||
// CRDB: https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L196-L201
|
||||
|
||||
// Refer to [Using Cert Manager to Deploy TLS for Postgres on Kubernetes](https://www.crunchydata.com/blog/using-cert-manager-to-deploy-tls-for-postgres-on-kubernetes)
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let SelfSigned = "\(_DBName)-selfsigned"
|
||||
let RootCA = "\(_DBName)-root-ca"
|
||||
let Orgs = ["Database"]
|
||||
|
||||
let Objects = {
|
||||
Name: "backstage-certs"
|
||||
Namespace: "backstage"
|
||||
|
||||
Resources: {
|
||||
// Put everything in the same namespace.
|
||||
[_]: {
|
||||
[NAME=_]: {
|
||||
metadata: name: NAME
|
||||
metadata: namespace: Namespace
|
||||
}
|
||||
}
|
||||
|
||||
Issuer: {
|
||||
"\(SelfSigned)": is.#Issuer & {
|
||||
_description: "Self signed issuer to issue ca certs"
|
||||
metadata: name: SelfSigned
|
||||
spec: selfSigned: {}
|
||||
}
|
||||
"\(RootCA)": is.#Issuer & {
|
||||
_description: "Root signed intermediate ca to issue mtls database certs"
|
||||
metadata: name: RootCA
|
||||
spec: ca: secretName: RootCA
|
||||
}
|
||||
}
|
||||
Certificate: {
|
||||
"\(RootCA)": crt.#Certificate & {
|
||||
_description: "Root CA cert for database"
|
||||
metadata: name: RootCA
|
||||
spec: {
|
||||
commonName: RootCA
|
||||
isCA: true
|
||||
issuerRef: group: "cert-manager.io"
|
||||
issuerRef: kind: "Issuer"
|
||||
issuerRef: name: SelfSigned
|
||||
privateKey: algorithm: "ECDSA"
|
||||
privateKey: size: 256
|
||||
secretName: RootCA
|
||||
subject: organizations: Orgs
|
||||
}
|
||||
}
|
||||
"\(_DBName)-primary-tls": #DatabaseCert & {
|
||||
// PGO managed name is "<cluster name>-cluster-cert" e.g. zitadel-cluster-cert
|
||||
spec: {
|
||||
commonName: "\(_DBName)-primary"
|
||||
dnsNames: [
|
||||
commonName,
|
||||
"\(commonName).\(Namespace)",
|
||||
"\(commonName).\(Namespace).svc",
|
||||
"\(commonName).\(Namespace).svc.cluster.local",
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
]
|
||||
usages: ["digital signature", "key encipherment"]
|
||||
}
|
||||
}
|
||||
"\(_DBName)-repl-tls": #DatabaseCert & {
|
||||
spec: {
|
||||
commonName: "_crunchyrepl"
|
||||
dnsNames: [commonName]
|
||||
usages: ["digital signature", "key encipherment"]
|
||||
}
|
||||
}
|
||||
"\(_DBName)-client-tls": #DatabaseCert & {
|
||||
spec: {
|
||||
commonName: "\(_DBName)-client"
|
||||
dnsNames: [commonName]
|
||||
usages: ["digital signature", "key encipherment"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#DatabaseCert: crt.#Certificate & {
|
||||
metadata: name: string
|
||||
metadata: namespace: string
|
||||
spec: {
|
||||
duration: "2160h" // 90d
|
||||
renewBefore: "360h" // 15d
|
||||
issuerRef: group: "cert-manager.io"
|
||||
issuerRef: kind: "Issuer"
|
||||
issuerRef: name: RootCA
|
||||
privateKey: algorithm: "ECDSA"
|
||||
privateKey: size: 256
|
||||
secretName: metadata.name
|
||||
subject: organizations: Orgs
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Database Certs
|
||||
|
||||
This component issues postgres certificates using certmanager. Apply this component to the management cluster.
|
||||
|
||||
The purpose is to define customTLSSecret and customReplicationTLSSecret to provide certs that allow the standby to authenticate to the primary. For this type of standby, you must use custom TLS.
|
||||
|
||||
Refer to the PGO [Streaming Standby](https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#streaming-standby) tutorial.
|
||||
@@ -0,0 +1,112 @@
|
||||
package holos
|
||||
|
||||
// TODO This entire config should be removed for clarity. Only the production
|
||||
// config should be referenced by the Deployment.
|
||||
|
||||
_BackstageAppConfig: {
|
||||
app: {
|
||||
title: "Holos Portal"
|
||||
baseUrl: "${BASE_URL}"
|
||||
}
|
||||
|
||||
organization: name: "My Company"
|
||||
|
||||
backend: {
|
||||
// Used for enabling authentication, secret is shared by all backend plugins
|
||||
// See https://backstage.io/docs/auth/service-to-service-auth for
|
||||
// information on the format
|
||||
// auth:
|
||||
// keys:
|
||||
// - secret: ${BACKEND_SECRET}
|
||||
baseUrl: "${BASE_URL}"
|
||||
listen: port: 7007
|
||||
// Uncomment the following host directive to bind to specific interfaces
|
||||
// host: 127.0.0.1
|
||||
csp: {
|
||||
"connect-src": ["'self'", "http:", "https:"]
|
||||
}
|
||||
// Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
|
||||
// Default Helmet Content-Security-Policy values can be removed by setting the key to false
|
||||
cors: {
|
||||
origin: "${BASE_URL}"
|
||||
methods: ["GET", "HEAD", "PATCH", "POST", "PUT", "DELETE"]
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
// workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
|
||||
|
||||
integrations: {
|
||||
github: [{
|
||||
host: "github.com"
|
||||
// This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information
|
||||
// about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration
|
||||
token: "${GITHUB_TOKEN}"
|
||||
}]
|
||||
}
|
||||
//## Example for how to add your GitHub Enterprise instance using the API:
|
||||
// - host: ghe.example.net
|
||||
// apiBaseUrl: https://ghe.example.net/api/v3
|
||||
// token: ${GHE_TOKEN}
|
||||
|
||||
proxy: null
|
||||
//## Example for how to add a proxy endpoint for the frontend.
|
||||
//## A typical reason to do this is to handle HTTPS and CORS for internal services.
|
||||
// endpoints:
|
||||
// '/test':
|
||||
// target: 'https://example.com'
|
||||
// changeOrigin: true
|
||||
// Reference documentation http://backstage.io/docs/features/techdocs/configuration
|
||||
// Note: After experimenting with basic setup, use CI/CD to generate docs
|
||||
// and an external cloud storage when deploying TechDocs for production use-case.
|
||||
// https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach
|
||||
techdocs: {
|
||||
builder: "local" // Alternatives - 'external'
|
||||
generator: {
|
||||
runIn: "docker"
|
||||
} // Alternatives - 'local'
|
||||
publisher: {
|
||||
type: "local"
|
||||
}
|
||||
} // Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
|
||||
|
||||
auth: {
|
||||
environment: "development"
|
||||
// see https://backstage.io/docs/auth/ to learn about auth providers
|
||||
providers: {
|
||||
// See https://backstage.io/docs/auth/guest/provider
|
||||
guest: {}
|
||||
}
|
||||
}
|
||||
|
||||
scaffolder: null
|
||||
// see https://backstage.io/docs/features/software-templates/configuration for software template options
|
||||
|
||||
catalog: {
|
||||
import: {
|
||||
entityFilename: "catalog-info.yaml"
|
||||
pullRequestBranchName: "backstage-integration"
|
||||
}
|
||||
rules: [{allow: ["Component", "System", "API", "Resource", "Location"]}]
|
||||
locations: [
|
||||
{
|
||||
// Local example data, file locations are relative to the backend process, typically `packages/backend`
|
||||
type: "file"
|
||||
target: "../../examples/entities.yaml"
|
||||
},
|
||||
{
|
||||
// Local example template
|
||||
type: "file"
|
||||
target: "../../examples/template/template.yaml"
|
||||
rules: [{
|
||||
allow: ["Template"]}]
|
||||
},
|
||||
{
|
||||
// Local organizational data
|
||||
type: "file"
|
||||
target: "../../org.yaml"
|
||||
rules: [{
|
||||
allow: ["User", "Group"]}]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package holos
|
||||
|
||||
// Imported from https://github.com/holos-run/portal/blob/d5127715fb4710b9b272768e6a1ce2ff122e693e/app-config.production.yaml
|
||||
|
||||
_BackstageProductionConfig: {
|
||||
app: {
|
||||
// Should be the same as backend.baseUrl when using the `app-backend` plugin.
|
||||
baseUrl: "${BASE_URL}"
|
||||
}
|
||||
|
||||
backend: {
|
||||
// Note that the baseUrl should be the URL that the browser and other clients
|
||||
// should use when communicating with the backend, i.e. it needs to be
|
||||
// reachable not just from within the backend host, but from all of your
|
||||
// callers. When its value is "http://localhost:7007", it's strictly private
|
||||
// and can't be reached by others.
|
||||
baseUrl: "${BASE_URL}"
|
||||
// The listener can also be expressed as a single <host>:<port> string. In this case we bind to
|
||||
// all interfaces, the most permissive setting. The right value depends on your specific deployment.
|
||||
listen: ":7007"
|
||||
|
||||
// config options: https://node-postgres.com/api/client
|
||||
database: {
|
||||
client: "pg"
|
||||
connection: {
|
||||
host: "${POSTGRES_HOST}"
|
||||
port: "${POSTGRES_PORT}"
|
||||
user: "${POSTGRES_USER}"
|
||||
password: "${POSTGRES_PASSWORD}"
|
||||
ssl: ca: "${PGBOUNCER_CA_ROOT}"
|
||||
}
|
||||
}
|
||||
|
||||
reading: allow: [{
|
||||
host: "holos.run"
|
||||
}, {
|
||||
host: "*.holos.run"
|
||||
}, {
|
||||
host: "openinfrastructure.co"
|
||||
}, {
|
||||
host: "*.openinfrastructure.co"
|
||||
}]
|
||||
}
|
||||
|
||||
auth: {
|
||||
environment: "production"
|
||||
providers: {
|
||||
guest: null
|
||||
holosProxy: {
|
||||
issuer: "https://login.holos.run"
|
||||
audience: "269746002573969304"
|
||||
oidcIdTokenHeader: "x-oidc-id-token"
|
||||
signIn: resolvers: [{
|
||||
resolver: "emailMatchingUserEntityProfileEmail"
|
||||
}, {
|
||||
resolver: "signInWithoutCatalogUser"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catalog: {
|
||||
// Overrides the default list locations from app-config.yaml
|
||||
// Refer to https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog
|
||||
//
|
||||
// NOTE: In production, CWD is /app This is different than development where
|
||||
// CWD is ./packages/backend, As a result, entries cannot be copied verbatim
|
||||
// from app-config.yaml otherwise they will not resolve to the correct location.
|
||||
locations: [{
|
||||
// Initial iam User and Group data is expected to be provided in the Secret.
|
||||
// Local example data, file locations are relative to the backend process, typically `packages/backend`
|
||||
type: "file"
|
||||
target: "/config/iam.yaml"
|
||||
rules: [{
|
||||
allow: ["User", "Group"]}]
|
||||
}]
|
||||
|
||||
// GitHub Discovery
|
||||
// Refer to https://backstage.io/docs/integrations/github/discovery/#configuration
|
||||
providers: {
|
||||
github: {
|
||||
primaryOrg: {
|
||||
organization: string & _Platform.Model.github.primaryOrg
|
||||
catalogPath: "/catalog-info.yaml"
|
||||
filters: {
|
||||
branch: "main"
|
||||
repository: ".*" // Regex
|
||||
}
|
||||
// same options as in TaskScheduleDefinition
|
||||
schedule: {
|
||||
// supports cron, ISO duration, "human duration" as used in code
|
||||
frequency: minutes: 30
|
||||
// supports ISO duration, "human duration" as used in code
|
||||
timeout: minutes: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refers to ExternalSecret github-app-credentials. See the readme.md for how this secret is produced.
|
||||
integrations: {
|
||||
github: [{
|
||||
host: "github.com"
|
||||
apps: [{"$include": "/secrets/github-app-credentials/github-app-credentials.yaml"}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package holos
|
||||
|
||||
import "encoding/yaml"
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let ContainerPort = _Component.spec.port
|
||||
|
||||
let Objects = {
|
||||
Name: "\(_Component.metadata.name)-backend"
|
||||
Namespace: _Component.metadata.namespace
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
let MatchLabels = {
|
||||
"app.kubernetes.io/name": _Component.metadata.name
|
||||
"app.kubernetes.io/instance": _Component.metadata.name
|
||||
"app.kubernetes.io/component": Name
|
||||
}
|
||||
|
||||
Resources: {
|
||||
// Grant the Gateway ns the ability to refer to the Service from HTTPRoutes.
|
||||
ReferenceGrant: (#IstioGatewaysNamespace): #ReferenceGrant
|
||||
|
||||
// For the Github integration.
|
||||
ExternalSecret: [_]: #ExternalSecret & {metadata: namespace: Namespace}
|
||||
ExternalSecret: githubAppCredentials: metadata: name: "github-app-credentials"
|
||||
|
||||
// Primary configuration for backstage to pull unified config data.
|
||||
ConfigMap: config: {
|
||||
metadata: namespace: Namespace
|
||||
metadata: name: Name
|
||||
data: {
|
||||
"app-config.yaml": yaml.Marshal(_BackstageAppConfig)
|
||||
"app-config.production.yaml": yaml.Marshal(_BackstageProductionConfig)
|
||||
"iam.yaml": yaml.MarshalStream([for x in _BackstageIAMConfig {x}])
|
||||
}
|
||||
}
|
||||
|
||||
Deployment: backstage: {
|
||||
metadata: labels: MatchLabels
|
||||
spec: {
|
||||
selector: matchLabels: MatchLabels
|
||||
|
||||
template: {
|
||||
metadata: labels: "sidecar.istio.io/inject": "true"
|
||||
metadata: labels: MatchLabels
|
||||
spec: {
|
||||
securityContext: seccompProfile: type: "RuntimeDefault"
|
||||
serviceAccountName: "default"
|
||||
containers: [{
|
||||
name: "backstage-backend"
|
||||
image: "quay.io/holos-run/portal:latest"
|
||||
imagePullPolicy: "Always"
|
||||
// https://github.com/backstage/backstage/blob/v1.27.6/packages/create-app/templates/default-app/packages/backend/Dockerfile#L52
|
||||
command: [
|
||||
"node",
|
||||
"packages/backend",
|
||||
"--config",
|
||||
"/config/app-config.yaml",
|
||||
"--config",
|
||||
"/config/app-config.production.yaml",
|
||||
]
|
||||
// Refer to https://backstage.io/docs/conf/writing#environment-variable-overrides
|
||||
//
|
||||
// Individual configuration values can be overridden using
|
||||
// environment variables prefixed with APP_CONFIG_. Everything
|
||||
// following that prefix in the environment variable name will be
|
||||
// used as the config key, with _ replaced by .. For example, to
|
||||
// override the app.baseUrl value, set the APP_CONFIG_app_baseUrl
|
||||
// environment variable to the desired value.
|
||||
//
|
||||
// The value of the environment variable is parsed as JSON, but it will fall back
|
||||
// to being interpreted as a string if it fails to parse. Note that if you for
|
||||
// example want to pass on the string "false", you need to wrap it in double
|
||||
// quotes, e.g. export APP_CONFIG_example='"false"'.
|
||||
env: [
|
||||
{
|
||||
name: "BASE_URL"
|
||||
value: "https://" + _Component.spec.hostname
|
||||
},
|
||||
{
|
||||
name: "ORG_DOMAIN"
|
||||
value: _Platform.Model.org.domain
|
||||
},
|
||||
{
|
||||
name: "POSTGRES_HOST"
|
||||
valueFrom: secretKeyRef: {
|
||||
name: "\(_DBName)-pguser-\(_DBName)-admin"
|
||||
key: "pgbouncer-host"
|
||||
optional: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "POSTGRES_PORT"
|
||||
valueFrom: secretKeyRef: {
|
||||
name: "\(_DBName)-pguser-\(_DBName)-admin"
|
||||
key: "pgbouncer-port"
|
||||
optional: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "POSTGRES_USER"
|
||||
valueFrom: secretKeyRef: {
|
||||
name: "\(_DBName)-pguser-\(_DBName)-admin"
|
||||
key: "user"
|
||||
optional: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "POSTGRES_PASSWORD"
|
||||
valueFrom: secretKeyRef: {
|
||||
name: "\(_DBName)-pguser-\(_DBName)-admin"
|
||||
key: "password"
|
||||
optional: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "PGBOUNCER_CA_ROOT"
|
||||
valueFrom: secretKeyRef: {
|
||||
name: "\(_DBName)-pgbouncer"
|
||||
key: "pgbouncer-frontend.ca-roots"
|
||||
optional: false
|
||||
}
|
||||
},
|
||||
]
|
||||
ports: [{
|
||||
name: "backend"
|
||||
containerPort: ContainerPort
|
||||
protocol: "TCP"
|
||||
}]
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "config"
|
||||
mountPath: "/config"
|
||||
},
|
||||
{
|
||||
name: "github-app-credentials"
|
||||
mountPath: "/secrets/github-app-credentials"
|
||||
},
|
||||
]
|
||||
}]
|
||||
volumes: [
|
||||
{
|
||||
name: "config"
|
||||
configMap: name: ConfigMap.config.metadata.name
|
||||
},
|
||||
{
|
||||
name: "github-app-credentials"
|
||||
secret: secretName: ExternalSecret.githubAppCredentials.metadata.name
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Service: backstage: {
|
||||
metadata: labels: MatchLabels
|
||||
spec: {
|
||||
selector: MatchLabels
|
||||
_ports: http: {
|
||||
port: ContainerPort
|
||||
targetPort: ContainerPort
|
||||
protocol: "TCP"
|
||||
name: "http"
|
||||
}
|
||||
ports: [for x in _ports {x}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package holos
|
||||
|
||||
_BackstageIAMConfig: {
|
||||
groupAdmin: {
|
||||
// https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group
|
||||
apiVersion: "backstage.io/v1alpha1"
|
||||
kind: "Group"
|
||||
metadata: name: "prod-cluster-admin"
|
||||
spec: {
|
||||
type: "team"
|
||||
children: []
|
||||
}
|
||||
}
|
||||
|
||||
user1: {
|
||||
// https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user
|
||||
apiVersion: "backstage.io/v1alpha1"
|
||||
kind: "User"
|
||||
metadata: name: "jeff"
|
||||
spec: {
|
||||
profile: email: "jeff@openinfrastructure.co"
|
||||
memberOf: ["prod-cluster-admin"]
|
||||
}
|
||||
}
|
||||
|
||||
user2: {
|
||||
// https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user
|
||||
apiVersion: "backstage.io/v1alpha1"
|
||||
kind: "User"
|
||||
metadata: name: "gary"
|
||||
spec: {
|
||||
profile: email: "gary@openinfrastructure.co"
|
||||
memberOf: ["prod-cluster-admin"]
|
||||
}
|
||||
}
|
||||
|
||||
user3: {
|
||||
// https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user
|
||||
apiVersion: "backstage.io/v1alpha1"
|
||||
kind: "User"
|
||||
metadata: name: "nate"
|
||||
spec: {
|
||||
profile: email: "nate@openinfrastructure.co"
|
||||
memberOf: ["prod-cluster-admin"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Backstage Secrets
|
||||
|
||||
Backstage needs secrets in place on the management cluster to operate.
|
||||
|
||||
ExternalSecret `github-app-credentials` generated using:
|
||||
|
||||
Refer to the [portal](https://github.com/holos-run/portal) repo.
|
||||
|
||||
```sh
|
||||
# portal is your backstage repository created with `npx @backstage/create-app`
|
||||
cd portal
|
||||
|
||||
# my-org is your github organization.
|
||||
yarn backstage-cli create-github-app my-org
|
||||
|
||||
# Create the secret in your management cluster.
|
||||
mv github-app-backstage-*-credentials.yaml github-app-credentials.yaml
|
||||
holos create secret -n backstage --append-hash=false --from-file=github-app-credentials.yaml github-app-credentials
|
||||
|
||||
# Remove the secret from the local host.
|
||||
rm -f github-app-credentials.yaml
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
// Restore from backup. Flip this to true after the database is provisioned and
|
||||
// a backup has been taken.
|
||||
let RestoreFromBackup = false
|
||||
|
||||
// The Secret containing the pgbackrest s3.conf file.
|
||||
let S3Secret = "pgbackrest"
|
||||
|
||||
let Cluster = _Clusters[_ClusterName]
|
||||
|
||||
let DatabaseUser = _DBName
|
||||
let DatabaseAdmin = "\(_DBName)-admin"
|
||||
|
||||
// This must be an external storage bucket for our architecture.
|
||||
let BucketRepoName = "repo2"
|
||||
|
||||
// Restore options. Set the timestamp to a known good point in time.
|
||||
// time="2024-03-11T17:08:58Z" level=info msg="crunchy-pgbackrest ends"
|
||||
// let RestoreOptions = ["--type=time", "--target=\"2024-03-11 17:10:00+00\""]
|
||||
|
||||
// Restore the most recent backup.
|
||||
let RestoreOptions = []
|
||||
|
||||
let Objects = {
|
||||
Name: "backstage-database"
|
||||
Namespace: "backstage"
|
||||
|
||||
Resources: {
|
||||
// All resources go into the same namespace
|
||||
[_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
PostgresCluster: db: HighlyAvailable & {
|
||||
metadata: name: _DBName
|
||||
spec: {
|
||||
image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.2-0"
|
||||
postgresVersion: 16
|
||||
// Custom certs are necessary for streaming standby replication which we use to replicate between two regions.
|
||||
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/disaster-recovery#streaming-standby
|
||||
customTLSSecret: name: "\(_DBName)-primary-tls"
|
||||
customReplicationTLSSecret: name: "\(_DBName)-repl-tls"
|
||||
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/references/crd/5.5.x/postgrescluster#postgresclusterspecusersindex
|
||||
users: [
|
||||
{name: DatabaseUser},
|
||||
// NOTE: Users with SUPERUSER role cannot log in through pgbouncer. Use options that allow zitadel admin to use pgbouncer.
|
||||
// Refer to: https://github.com/CrunchyData/postgres-operator/issues/3095#issuecomment-1904712211
|
||||
{name: DatabaseAdmin, options: "CREATEDB CREATEROLE", databases: [_DBName, "postgres"]},
|
||||
]
|
||||
users: [...{databases: [_DBName, ...]}]
|
||||
instances: [{
|
||||
replicas: 2
|
||||
dataVolumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: "20Gi"
|
||||
}
|
||||
}]
|
||||
standby: {
|
||||
repoName: BucketRepoName
|
||||
if Cluster.primary {
|
||||
enabled: false
|
||||
}
|
||||
if !Cluster.primary {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
// Monitoring configuration
|
||||
monitoring: pgmonitor: exporter: image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres-exporter:ubi8-5.5.1-0"
|
||||
// Restore from backup if and only if the cluster is primary and
|
||||
// RestoreFromBackup has transitioned from false to true.
|
||||
if Cluster.primary && RestoreFromBackup {
|
||||
dataSource: pgbackrest: {
|
||||
stanza: "db"
|
||||
configuration: backups.pgbackrest.configuration
|
||||
// Restore from known good full backup taken
|
||||
options: RestoreOptions
|
||||
global: {
|
||||
"\(BucketRepoName)-path": "/pgbackrest/\(metadata.namespace)/\(metadata.name)/\(BucketRepoName)"
|
||||
"\(BucketRepoName)-cipher-type": "aes-256-cbc"
|
||||
}
|
||||
repo: {
|
||||
name: BucketRepoName
|
||||
s3: backups.pgbackrest.repos[1].s3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups
|
||||
backups: pgbackrest: {
|
||||
configuration: [{secret: name: S3Secret}]
|
||||
// Defines details for manual pgBackRest backup Jobs
|
||||
manual: {
|
||||
// Note: the repoName value must match the config keys in the S3Secret.
|
||||
// This must be an external repository for backup / restore / regional failovers.
|
||||
repoName: BucketRepoName
|
||||
options: ["--type=full", ...]
|
||||
}
|
||||
// Defines details for performing an in-place restore using pgBackRest
|
||||
restore: {
|
||||
// Enables triggering a restore by annotating the postgrescluster with postgres-operator.crunchydata.com/pgbackrest-restore="$(date)"
|
||||
enabled: true
|
||||
repoName: BucketRepoName
|
||||
}
|
||||
global: {
|
||||
// Store only one full backup in the PV because it's more expensive than object storage.
|
||||
"\(repos[0].name)-retention-full": "1"
|
||||
// Store 14 days of full backups in the bucket.
|
||||
"\(BucketRepoName)-retention-full": string | *"14"
|
||||
"\(BucketRepoName)-retention-full-type": "count" | *"time" // time in days
|
||||
// Refer to https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#encryption
|
||||
"\(BucketRepoName)-cipher-type": "aes-256-cbc"
|
||||
// "The convention we recommend for setting this variable is /pgbackrest/$NAMESPACE/$CLUSTER_NAME/repoN"
|
||||
// Ref: https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery/backups#understanding-backup-configuration-and-basic-operations
|
||||
"\(BucketRepoName)-path": "/pgbackrest/\(metadata.namespace)/\(metadata.name)/\(manual.repoName)"
|
||||
}
|
||||
repos: [
|
||||
{
|
||||
name: "repo1"
|
||||
volume: volumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: string | *"4Gi"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: BucketRepoName
|
||||
// Full backup weekly on Sunday at 1am, differntial daily at 1am every day except Sunday.
|
||||
schedules: full: string | *"0 1 * * 0"
|
||||
schedules: differential: string | *"0 1 * * 1-6"
|
||||
s3: {
|
||||
bucket: _BackupBucket.metadata.name
|
||||
region: _BackupBucket.spec.region
|
||||
endpoint: "s3.dualstack.\(region).amazonaws.com"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refer to https://github.com/holos-run/postgres-operator-examples/blob/main/kustomize/high-availability/ha-postgres.yaml
|
||||
let HighlyAvailable = {
|
||||
apiVersion: "postgres-operator.crunchydata.com/v1beta1"
|
||||
kind: "PostgresCluster"
|
||||
metadata: name: string
|
||||
spec: {
|
||||
image: "registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.2-0"
|
||||
postgresVersion: 16
|
||||
instances: [{
|
||||
name: "pgha1"
|
||||
replicas: 2
|
||||
dataVolumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: string | *"20Gi"
|
||||
}
|
||||
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||
weight: 1
|
||||
podAffinityTerm: {
|
||||
topologyKey: "topology.kubernetes.io/zone"
|
||||
labelSelector: matchLabels: {
|
||||
"postgres-operator.crunchydata.com/cluster": metadata.name
|
||||
"postgres-operator.crunchydata.com/instance-set": name
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
backups: pgbackrest: {
|
||||
image: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.49-0"
|
||||
}
|
||||
proxy: pgBouncer: {
|
||||
image: "registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:ubi8-1.21-3"
|
||||
replicas: 2
|
||||
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||
weight: 1
|
||||
podAffinityTerm: {
|
||||
topologyKey: "topology.kubernetes.io/zone"
|
||||
labelSelector: matchLabels: {
|
||||
"postgres-operator.crunchydata.com/cluster": metadata.name
|
||||
"postgres-operator.crunchydata.com/role": "pgbouncer"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let Objects = {
|
||||
Name: "\(_Component.metadata.name)-routes"
|
||||
Namespace: #IstioGatewaysNamespace
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
|
||||
Resources: HTTPRoute: (_Component.metadata.name): {
|
||||
spec: hostnames: [_Component.spec.hostname]
|
||||
spec: parentRefs: [{
|
||||
name: "default"
|
||||
namespace: #IstioGatewaysNamespace
|
||||
}]
|
||||
spec: rules: [
|
||||
{
|
||||
matches: [{path: {type: "PathPrefix", value: "/"}}]
|
||||
backendRefs: [{
|
||||
name: _Component.metadata.name
|
||||
namespace: _Component.metadata.namespace
|
||||
port: _Component.spec.port
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let Objects = {
|
||||
Name: "backstage-secrets"
|
||||
Namespace: "backstage"
|
||||
|
||||
Resources: {
|
||||
ExternalSecret: [_]: #ExternalSecret & {metadata: namespace: Namespace}
|
||||
|
||||
ExternalSecret: "\(_DBName)-primary-tls": #ExternalCert
|
||||
ExternalSecret: "\(_DBName)-repl-tls": #ExternalCert
|
||||
ExternalSecret: "\(_DBName)-client-tls": #ExternalCert
|
||||
ExternalSecret: "\(_DBName)-root-ca": #ExternalCert
|
||||
|
||||
ExternalSecret: "pgbackrest": _
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package holos
|
||||
|
||||
import (
|
||||
cpv1 "pkg.crossplane.io/provider/v1"
|
||||
cpdrcv1beta1 "pkg.crossplane.io/deploymentruntimeconfig/v1beta1"
|
||||
cpfuncv1beta1 "pkg.crossplane.io/function/v1beta1"
|
||||
cpawspcv1beta1 "aws.upbound.io/providerconfig/v1beta1"
|
||||
)
|
||||
|
||||
// Produce a helm chart build plan.
|
||||
(#Helm & Chart).Output
|
||||
|
||||
// https://github.com/crossplane/crossplane/releases
|
||||
let CrossplaneVersion = "1.16.0"
|
||||
|
||||
// https://github.com/crossplane-contrib/provider-upjet-aws/releases
|
||||
let AWSProviderVersion = "v1.5.0"
|
||||
|
||||
let Chart = {
|
||||
Name: "crossplane"
|
||||
Version: CrossplaneVersion
|
||||
Namespace: "crossplane-system"
|
||||
|
||||
Repo: name: "crossplane-stable"
|
||||
Repo: url: "https://charts.crossplane.io/stable"
|
||||
|
||||
Values: {
|
||||
podSecurityContextCrossplane: {
|
||||
runAsNonRoot: true
|
||||
seccompProfile: type: "RuntimeDefault"
|
||||
}
|
||||
securityContextCrossplane: capabilities: drop: ["ALL"]
|
||||
podSecurityContextRBACManager: {
|
||||
runAsNonRoot: true
|
||||
seccompProfile: type: "RuntimeDefault"
|
||||
}
|
||||
securityContextRBACManager: capabilities: drop: ["ALL"]
|
||||
}
|
||||
|
||||
Resources: {
|
||||
// This DeploymentRuntimeConfig adds annotations to the service accounts spun up for the AWS providers.
|
||||
// https://docs.crossplane.io/latest/concepts/providers/#configuring-metadata-of-runtime-resources
|
||||
//
|
||||
// Adding this SA annotation causes the EKS Pod Identity Webhook to inject
|
||||
// environment variables for AWS authentication as well as mount the AWS token
|
||||
// file to the provider pod that uses the SA. For example:
|
||||
//
|
||||
// Environment:
|
||||
// AWS_STS_REGIONAL_ENDPOINTS: regional
|
||||
// AWS_DEFAULT_REGION: us-east-1
|
||||
// AWS_REGION: us-east-1
|
||||
// AWS_ROLE_ARN: arn:aws:iam::271053619184:role/holos-crossplane
|
||||
// AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
|
||||
// Mounts:
|
||||
// /var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)
|
||||
//
|
||||
// Docs: https://docs.crossplane.io/latest/concepts/providers/#runtime-configuration
|
||||
DeploymentRuntimeConfig: "aws-irsa": cpdrcv1beta1.#DeploymentRuntimeConfig & {
|
||||
metadata: name: "aws-irsa"
|
||||
spec: serviceAccountTemplate: metadata: annotations: "eks.amazonaws.com/role-arn": "arn:aws:iam::\(_Platform.Model.aws.accountNumber):role/holos-crossplane"
|
||||
spec: deploymentTemplate: spec: template: spec: containers: [
|
||||
{
|
||||
name: "package-runtime"
|
||||
args: ["--enable-external-secret-stores"]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// https://marketplace.upbound.io/providers/upbound/provider-family-aws
|
||||
Provider: "upbound-provider-family-aws": cpv1.#Provider & {
|
||||
metadata: name: "upbound-provider-family-aws"
|
||||
spec: package: "xpkg.upbound.io/upbound/provider-family-aws:\(AWSProviderVersion)"
|
||||
// The provider-family-aws provider doesn't need the IRSA SA annotation.
|
||||
spec: runtimeConfigRef: name: "default"
|
||||
}
|
||||
// https://marketplace.upbound.io/providers/upbound/provider-aws-s3
|
||||
Provider: "provider-aws-s3": cpv1.#Provider & {
|
||||
metadata: name: "provider-aws-s3"
|
||||
spec: package: "xpkg.upbound.io/upbound/provider-aws-s3:\(AWSProviderVersion)"
|
||||
spec: runtimeConfigRef: name: "aws-irsa"
|
||||
}
|
||||
// https://marketplace.upbound.io/providers/upbound/provider-aws-rds
|
||||
Provider: "provider-aws-rds": cpv1.#Provider & {
|
||||
metadata: name: "provider-aws-rds"
|
||||
spec: package: "xpkg.upbound.io/upbound/provider-aws-rds:\(AWSProviderVersion)"
|
||||
spec: runtimeConfigRef: name: "aws-irsa"
|
||||
}
|
||||
|
||||
// The patch-and-transform function is used in Compositions.
|
||||
// https://github.com/crossplane-contrib/function-patch-and-transform/releases
|
||||
Function: "function-patch-and-transform": cpfuncv1beta1.#Function & {
|
||||
metadata: name: "function-patch-and-transform"
|
||||
spec: package: "xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.5.0"
|
||||
}
|
||||
|
||||
// By setting the ProviderConfig's name to "default", all resources with an
|
||||
// apiVersion of "aws.upbound.io/v1beta1" will use this ProviderConfig unless
|
||||
// otherwise specified with a providerConfigRef.
|
||||
ProviderConfig: default: cpawspcv1beta1.#ProviderConfig & {
|
||||
metadata: name: "default"
|
||||
spec: credentials: source: "IRSA"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package holos
|
||||
|
||||
// Produce a kubectl kustomize build plan.
|
||||
(#Kustomize & {Name: "crossplane_crds"}).Output
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
# Install the CRDs for Crossplane and the providers we use.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: "crossplane-system"
|
||||
resources:
|
||||
- https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/pkg.crossplane.io_deploymentruntimeconfigs.yaml
|
||||
- https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/pkg.crossplane.io_providers.yaml
|
||||
- https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/pkg.crossplane.io_functions.yaml
|
||||
- https://raw.githubusercontent.com/crossplane-contrib/provider-upjet-aws/v1.5.0/package/crds/aws.upbound.io_providerconfigs.yaml
|
||||
@@ -0,0 +1 @@
|
||||
package holos
|
||||
@@ -0,0 +1,206 @@
|
||||
package holos
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
)
|
||||
|
||||
let NAME = "ecr-creds-manager"
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
// The path Pod Identity uses.
|
||||
let MOUNT = "/var/run/secrets/eks.amazonaws.com/serviceaccount/"
|
||||
let AWS_ACCOUNT = _Platform.Model.aws.accountNumber
|
||||
let AWS_REGION = _Platform.Model.aws.primaryRegion
|
||||
let AWS_ROLE_ARN = "arn:aws:iam::\(AWS_ACCOUNT):role/\(NAME)"
|
||||
|
||||
let Objects = {
|
||||
Name: NAME
|
||||
Namespace: "holos-system"
|
||||
|
||||
Resources: {
|
||||
// Kubernetes ServiceAccount used by the Job.
|
||||
ServiceAccount: "\(Name)": corev1.#ServiceAccount & {
|
||||
metadata: {
|
||||
name: Name
|
||||
namespace: Namespace
|
||||
annotations: "holos.run/description": "Refreshes image pull credentials for use with AWS ECR."
|
||||
// annotations: "eks.amazonaws.com/role-arn": AWS_ROLE_ARN
|
||||
}
|
||||
}
|
||||
|
||||
// Job needs to read and write secrets across the cluster.
|
||||
ClusterRole: "\(Name)": rbacv1.#ClusterRole & {
|
||||
metadata: name: Name
|
||||
rules: [
|
||||
{
|
||||
apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["*"]
|
||||
},
|
||||
{
|
||||
apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["list"]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Bind the Role to the ServiceAccount for the Job.
|
||||
ClusterRoleBinding: "\(Name)": rbacv1.#ClusterRoleBinding & {
|
||||
metadata: name: Name
|
||||
roleRef: {
|
||||
apiGroup: "rbac.authorization.k8s.io"
|
||||
kind: "ClusterRole"
|
||||
name: Name
|
||||
}
|
||||
subjects: [
|
||||
{
|
||||
kind: "ServiceAccount"
|
||||
name: Name
|
||||
namespace: Namespace
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Make the CronJob and Job identical.
|
||||
let JobSpec = {
|
||||
serviceAccountName: Name
|
||||
restartPolicy: "OnFailure"
|
||||
securityContext: {
|
||||
seccompProfile: type: "RuntimeDefault"
|
||||
runAsNonRoot: true
|
||||
runAsUser: 8192 // app
|
||||
}
|
||||
nodeSelector: {
|
||||
"cloud.google.com/gke-spot": "true"
|
||||
"kubernetes.io/os": "linux"
|
||||
}
|
||||
containers: [
|
||||
{
|
||||
name: "toolkit"
|
||||
image: "quay.io/holos/toolkit:latest"
|
||||
securityContext: {
|
||||
capabilities: drop: ["ALL"]
|
||||
allowPrivilegeEscalation: false
|
||||
}
|
||||
command: ["/bin/bash"]
|
||||
args: ["/config/entrypoint"]
|
||||
env: [
|
||||
{
|
||||
name: "HOME"
|
||||
value: "/tmp"
|
||||
},
|
||||
{
|
||||
name: "AWS_DEFAULT_REGION"
|
||||
value: AWS_REGION
|
||||
},
|
||||
{
|
||||
name: "AWS_REGION"
|
||||
value: AWS_REGION
|
||||
},
|
||||
{
|
||||
name: "AWS_ROLE_ARN"
|
||||
value: AWS_ROLE_ARN
|
||||
},
|
||||
{
|
||||
name: "AWS_WEB_IDENTITY_TOKEN_FILE"
|
||||
value: MOUNT + "token"
|
||||
},
|
||||
{
|
||||
name: "AWS_STS_REGIONAL_ENDPOINTS"
|
||||
value: "regional"
|
||||
},
|
||||
]
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "config"
|
||||
mountPath: "/config"
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: "aws-token"
|
||||
mountPath: MOUNT
|
||||
readOnly: true
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
volumes: [
|
||||
{
|
||||
name: "config"
|
||||
configMap: name: Name
|
||||
},
|
||||
{
|
||||
name: "aws-token"
|
||||
projected: sources: [{
|
||||
serviceAccountToken: {
|
||||
path: "token"
|
||||
expirationSeconds: 3600
|
||||
audience: "sts.amazonaws.com"
|
||||
}
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Job: "\(Name)": batchv1.#Job & {
|
||||
metadata: {
|
||||
name: Name
|
||||
namespace: Namespace
|
||||
}
|
||||
spec: template: spec: JobSpec
|
||||
}
|
||||
|
||||
CronJob: "\(Name)": batchv1.#CronJob & {
|
||||
metadata: name: Name
|
||||
metadata: namespace: Namespace
|
||||
spec: {
|
||||
schedule: "0 */8 * * *"
|
||||
jobTemplate: spec: {
|
||||
template: spec: JobSpec
|
||||
backoffLimit: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfigMap: "\(Name)": corev1.#ConfigMap & {
|
||||
metadata: name: Name
|
||||
metadata: namespace: Namespace
|
||||
data: entrypoint: ENTRYPOINT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ENTRYPOINT = """
|
||||
#! /bin/bash
|
||||
#
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
finish() {
|
||||
rm -rf "${tmpdir}"
|
||||
}
|
||||
trap finish EXIT
|
||||
|
||||
set -xeuo pipefail
|
||||
|
||||
aws sts get-caller-identity
|
||||
|
||||
aws ecr get-login-password --region \(AWS_REGION) \\
|
||||
| docker login --username AWS --password-stdin \(AWS_ACCOUNT).dkr.ecr.\(AWS_REGION).amazonaws.com
|
||||
|
||||
kubectl create secret docker-registry ecr-creds-\(AWS_ACCOUNT) \\
|
||||
--from-file=.dockerconfigjson=${HOME}/.docker/config.json \\
|
||||
--dry-run=client -o yaml \\
|
||||
> "${tmpdir}/secret.yaml"
|
||||
|
||||
# Copy the secret to all namespaces
|
||||
for ns in $(kubectl -o=jsonpath='{.items[*].metadata.name}' get namespaces); do
|
||||
echo -n "Copying secret to namespace ${ns}: "
|
||||
kubectl -n $ns apply --server-side=true -f "${tmpdir}/secret.yaml" || continue
|
||||
echo "Usage: "kubectl -n $ns patch serviceaccount default -p "'"'{"imagePullSecrets": [{"name": "ecr-creds-\(AWS_ACCOUNT)"}]}'"'"
|
||||
done
|
||||
"""
|
||||
@@ -0,0 +1,33 @@
|
||||
# ECR Credentials Manager
|
||||
|
||||
This component manages a `ecr-creds-refresher` `CronJob` in the `holos-system` `Namespace` of the Management Cluster. This job authenticates to AWS using workload identity. Refer to [Use workload identity with AWS](https://cloud.google.com/kubernetes-engine/multi-cloud/docs/aws/how-to/use-workload-identity-aws) for information on how to configure AWS to accepts kubernetes service account tokens from the GKE Management Cluster.
|
||||
|
||||
Refer also to [Pod Identity](https://github.com/aws/amazon-eks-pod-identity-webhook?tab=readme-ov-file#eks-walkthrough)
|
||||
|
||||
> [!NOTE]
|
||||
> Both documents refer to EKS, but the process is the same and works on any kubernetes cluster.
|
||||
|
||||
Example [trust policy][trust-policy]:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Federated": "arn:aws:iam::637423192589:oidc-provider/container.googleapis.com/v1/projects/holos-ops/locations/us-central1/clusters/management"
|
||||
},
|
||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"container.googleapis.com/v1/projects/holos-ops/locations/us-central1/clusters/management:aud": "sts.amazonaws.com",
|
||||
"container.googleapis.com/v1/projects/holos-ops/locations/us-central1/clusters/management:sub": "system:serviceaccount:holos-system:ecr-creds-manager"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
[trust-policy]: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-logic-multiple-context-keys-or-values.html
|
||||
@@ -0,0 +1,20 @@
|
||||
package holos
|
||||
|
||||
let NAME = "ecr-creds-refresher"
|
||||
|
||||
// Produce a kubernetes objects build plan.
|
||||
(#Kubernetes & Objects).Output
|
||||
|
||||
let AWS_ACCOUNT = _Platform.Model.aws.accountNumber
|
||||
|
||||
let Objects = {
|
||||
Name: NAME
|
||||
Namespace: "default"
|
||||
|
||||
for Namespace in _Namespaces {
|
||||
Resources: ExternalSecret: "\(Namespace.metadata.name)/ecr-creds-\(AWS_ACCOUNT)": #ExternalSecret & {
|
||||
metadata: name: "ecr-creds-\(AWS_ACCOUNT)"
|
||||
metadata: namespace: Namespace.metadata.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// This sets up the AWS EKS Pod Identity Webhook, which is used to inject AWS
|
||||
// credentials into pods so services can use IRSA for AWS authentication.
|
||||
//
|
||||
// See: https://github.com/aws/amazon-eks-pod-identity-webhook
|
||||
//
|
||||
// There isn't an official Helm chart for the EKS Pod Identity Webhook, so we
|
||||
// use https://github.com/jkroepke/helm-charts/tree/main/charts/amazon-eks-pod-identity-webhook
|
||||
// See: https://github.com/aws/amazon-eks-pod-identity-webhook/issues/4
|
||||
|
||||
package holos
|
||||
|
||||
// https://github.com/jkroepke/helm-charts/tree/main/charts/amazon-eks-pod-identity-webhook
|
||||
let ChartVersion = "2.1.3"
|
||||
|
||||
// https://github.com/aws/amazon-eks-pod-identity-webhook/releases
|
||||
// https://registry.hub.docker.com/r/amazon/amazon-eks-pod-identity-webhook/tags
|
||||
let AppVersion = "v0.5.4"
|
||||
|
||||
let Chart = {
|
||||
Name: "amazon-eks-pod-identity-webhook"
|
||||
Version: ChartVersion
|
||||
Namespace: "aws-pod-identity"
|
||||
|
||||
Repo: name: "jkroepke"
|
||||
Repo: url: "https://jkroepke.github.io/helm-charts"
|
||||
|
||||
Values: {
|
||||
image: tag: AppVersion
|
||||
config: {
|
||||
tokenAudience: "sts.amazonaws.com"
|
||||
defaultAwsRegion: _Platform.Model.aws.primaryRegion
|
||||
extraArgs: ["-v=4"] // verbosity of at least 4 is needed to see mutation events.
|
||||
}
|
||||
securityContext: {
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
}
|
||||
podSecurityContext: {
|
||||
// https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/README.md#usage-with-non-root-container-user
|
||||
fsGroup: 2000
|
||||
seccompProfile: type: "RuntimeDefault"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a helm chart build plan.
|
||||
(#Helm & Chart).Output
|
||||
@@ -21,6 +21,16 @@ let Objects = {
|
||||
}
|
||||
}
|
||||
|
||||
// Manage a service account to prevent ArgoCD from pruning it.
|
||||
ServiceAccount: "default-istio": {
|
||||
metadata: namespace: Namespace
|
||||
metadata: labels: {
|
||||
"gateway.istio.io/managed": "istio.io-gateway-controller"
|
||||
"gateway.networking.k8s.io/gateway-name": "default"
|
||||
"istio.io/gateway-name": "default"
|
||||
}
|
||||
}
|
||||
|
||||
// The default gateway with all listeners attached to tls certs.
|
||||
Gateway: default: {
|
||||
metadata: {
|
||||
@@ -32,8 +42,10 @@ let Objects = {
|
||||
}
|
||||
spec: {
|
||||
// Work with a struct of listeners instead of a list.
|
||||
_listeners: (#WildcardListener & {Name: "admin", Selector: _Selector.GrantSubdomainAdmin}).Output
|
||||
_listeners: (#WildcardListener & {Name: "admin", Selector: _Selector.GrantSubdomainAdmin, Cluster: true}).Output
|
||||
_listeners: (#WildcardListener & {Name: "login", Selector: _Selector.GrantSubdomainLogin, Cluster: false}).Output
|
||||
_listeners: (#WildcardListener & {Name: "app", Selector: _Selector.GrantSubdomainApp, Cluster: false}).Output
|
||||
_listeners: (#WildcardListener & {Name: "app", Selector: _Selector.GrantSubdomainApp, Cluster: true}).Output
|
||||
listeners: [for x in _listeners {x}]
|
||||
}
|
||||
}
|
||||
@@ -46,16 +58,19 @@ let Objects = {
|
||||
Selector: matchLabels: {[string]: string}
|
||||
|
||||
_Hostname: string
|
||||
_Prefix: string
|
||||
if Cluster == true {
|
||||
_Hostname: "\(Name).\(_ClusterName).\(_Platform.Model.org.domain)"
|
||||
_Prefix: "region-\(Name)"
|
||||
}
|
||||
if Cluster == false {
|
||||
_Hostname: "\(Name).\(_Platform.Model.org.domain)"
|
||||
_Prefix: "global-\(Name)"
|
||||
}
|
||||
|
||||
Output: [NAME=string]: {name: NAME}
|
||||
Output: {
|
||||
"\(Name)-apex": {
|
||||
"\(_Prefix)-apex": {
|
||||
hostname: _Hostname
|
||||
port: 443
|
||||
protocol: "HTTPS"
|
||||
@@ -68,7 +83,7 @@ let Objects = {
|
||||
allowedRoutes: namespaces: from: "Selector"
|
||||
allowedRoutes: namespaces: selector: Selector
|
||||
}
|
||||
"\(Name)-prefix": {
|
||||
"\(_Prefix)-prefix": {
|
||||
hostname: "*.\(_Hostname)"
|
||||
port: 443
|
||||
protocol: "HTTPS"
|
||||
|
||||
@@ -10,8 +10,7 @@ let Objects = {
|
||||
// Constrain the metadata of all component resources.
|
||||
Resources: [_]: [_]: metadata: _HTTPBin.metadata
|
||||
|
||||
// Grant the Gateway namespace the ability to refer to the backend httpbin
|
||||
// service in HTTPRoutes.
|
||||
// Grant the Gateway ns the ability to refer to the Service from HTTPRoutes.
|
||||
Resources: ReferenceGrant: (#IstioGatewaysNamespace): #ReferenceGrant
|
||||
|
||||
Resources: {
|
||||
|
||||
@@ -7,17 +7,19 @@ let Objects = {
|
||||
Name: "authpolicy"
|
||||
Namespace: _AuthProxy.metadata.namespace
|
||||
|
||||
let Metadata = _IAP.metadata
|
||||
let Selector = {matchLabels: "istio.io/gateway-name": "default"}
|
||||
|
||||
Resources: [_]: [_]: metadata: namespace: Namespace
|
||||
Resources: [_]: [NAME=string]: {
|
||||
metadata: _IAP.metadata
|
||||
metadata: name: NAME
|
||||
metadata: namespace: Namespace
|
||||
}
|
||||
|
||||
// Auth policy resources represent the RequestAuthentication and
|
||||
// AuthorizationPolicy resources in the istio-gateways namespace governing the
|
||||
// default Gateway.
|
||||
Resources: {
|
||||
RequestAuthentication: (Name): {
|
||||
metadata: Metadata & {name: Name}
|
||||
spec: jwtRules: [{
|
||||
audiences: ["\(_AuthProxy.projectID)"]
|
||||
forwardOriginalToken: true
|
||||
@@ -30,7 +32,6 @@ let Objects = {
|
||||
AuthorizationPolicy: "\(Name)-custom": {
|
||||
_description: "Route all requests through the auth proxy by default"
|
||||
|
||||
metadata: Metadata & {name: "\(Name)-custom"}
|
||||
spec: {
|
||||
action: "CUSTOM"
|
||||
provider: name: _AuthProxy.provider
|
||||
@@ -58,5 +59,220 @@ let Objects = {
|
||||
selector: Selector
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-nothing": {
|
||||
_description: "Allow nothing"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-login": {
|
||||
_description: "Allow login"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
rules: [
|
||||
{
|
||||
to: [{
|
||||
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
|
||||
operation: hosts: [
|
||||
// Allow requests to the login service
|
||||
_AuthProxy.issuerHost,
|
||||
_AuthProxy.issuerHost + ":*",
|
||||
]
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-admin": {
|
||||
_description: "Allow cluster admin roles"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
rules: [
|
||||
{
|
||||
to: [{
|
||||
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
|
||||
operation: hosts: [
|
||||
// Allow authenticated users with cluster admin, edit, or view
|
||||
// roles to access admin interfaces.
|
||||
|
||||
// TODO(jeff): The set of admin services should be defined in a
|
||||
// nice root-level struct somewhere, probably as part of the
|
||||
// _Projects struct.
|
||||
"argocd.admin.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"argocd.admin.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
"httpbin.admin.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"httpbin.admin.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
"backstage.admin.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"backstage.admin.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
]
|
||||
}]
|
||||
when: [
|
||||
// Must be issued by the platform identity provider.
|
||||
{
|
||||
key: "request.auth.principal"
|
||||
values: [_AuthProxy.issuerURL + "/*"]
|
||||
},
|
||||
// Must be intended for an app within the Holos Platform ZITADEL project.
|
||||
{
|
||||
key: "request.auth.audiences"
|
||||
values: [_AuthProxy.projectID]
|
||||
},
|
||||
// Must be presented by the istio ExtAuthz auth proxy.
|
||||
{
|
||||
key: "request.auth.presenter"
|
||||
values: [_AuthProxy.clientID]
|
||||
},
|
||||
// Must have one of the listed roles.
|
||||
AdminRoleGroups,
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-holos-server": {
|
||||
_description: "Allow authenticated access to holos server"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
rules: [
|
||||
{
|
||||
to: [{
|
||||
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
|
||||
operation: hosts: [
|
||||
// Allow authenticated users with cluster admin, edit, or view
|
||||
// roles to access admin interfaces.
|
||||
|
||||
// TODO(jeff): The set of admin services should be defined in a
|
||||
// nice root-level struct somewhere, probably as part of the
|
||||
// _Projects struct.
|
||||
"app.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"app.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
"dev.app.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"dev.app.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
|
||||
"app.\(_Platform.Model.org.domain)",
|
||||
"app.\(_Platform.Model.org.domain):*",
|
||||
"dev.app.\(_Platform.Model.org.domain)",
|
||||
"dev.app.\(_Platform.Model.org.domain):*",
|
||||
]
|
||||
}]
|
||||
when: [
|
||||
// Must be issued by the platform identity provider.
|
||||
{
|
||||
key: "request.auth.principal"
|
||||
values: [_AuthProxy.issuerURL + "/*"]
|
||||
},
|
||||
// Must be intended for an app within the Holos Platform ZITADEL project.
|
||||
{
|
||||
key: "request.auth.audiences"
|
||||
values: [_AuthProxy.projectID]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-portal": {
|
||||
_description: "Allow portal access"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
rules: [
|
||||
{
|
||||
to: [{
|
||||
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
|
||||
operation: hosts: [
|
||||
"backstage.admin.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"backstage.admin.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
]
|
||||
}]
|
||||
when: [
|
||||
// Must be issued by the platform identity provider.
|
||||
{
|
||||
key: "request.auth.principal"
|
||||
values: [_AuthProxy.issuerURL + "/*"]
|
||||
},
|
||||
// Must be intended for an app within the Holos Platform ZITADEL project.
|
||||
{
|
||||
key: "request.auth.audiences"
|
||||
values: [_AuthProxy.projectID]
|
||||
},
|
||||
// Must be presented by the istio ExtAuthz auth proxy.
|
||||
{
|
||||
key: "request.auth.presenter"
|
||||
values: [_AuthProxy.clientID]
|
||||
},
|
||||
{
|
||||
key: "request.auth.claims[groups]"
|
||||
values: ["portal-view"]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
AuthorizationPolicy: "\(Name)-allow-argocd": {
|
||||
_description: "Allow portal access"
|
||||
|
||||
spec: {
|
||||
action: "ALLOW"
|
||||
selector: Selector
|
||||
rules: [
|
||||
{
|
||||
to: [{
|
||||
// Refer to https://istio.io/latest/docs/ops/best-practices/security/#writing-host-match-policies
|
||||
operation: hosts: [
|
||||
"argocd.admin.\(_ClusterName).\(_Platform.Model.org.domain)",
|
||||
"argocd.admin.\(_ClusterName).\(_Platform.Model.org.domain):*",
|
||||
]
|
||||
}]
|
||||
when: [
|
||||
// Must be issued by the platform identity provider.
|
||||
{
|
||||
key: "request.auth.principal"
|
||||
values: [_AuthProxy.issuerURL + "/*"]
|
||||
},
|
||||
// Must be intended for an app within the Holos Platform ZITADEL project.
|
||||
{
|
||||
key: "request.auth.audiences"
|
||||
values: [_AuthProxy.projectID]
|
||||
},
|
||||
// Must be presented by the istio ExtAuthz auth proxy.
|
||||
{
|
||||
key: "request.auth.presenter"
|
||||
values: [_AuthProxy.clientID]
|
||||
},
|
||||
{
|
||||
key: "request.auth.claims[groups]"
|
||||
values: ["argocd-view"]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let AdminRoleGroups = {
|
||||
key: "request.auth.claims[groups]"
|
||||
values: [
|
||||
"prod-cluster-admin",
|
||||
"prod-cluster-edit",
|
||||
"prod-cluster-view",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,10 +33,32 @@ let Objects = {
|
||||
metadata: ProxyMetadata
|
||||
data: "config.yaml": yaml.Marshal(AuthProxyConfig)
|
||||
let AuthProxyConfig = {
|
||||
injectResponseHeaders: [{
|
||||
name: _AuthProxy.idTokenHeader
|
||||
values: [{claim: "id_token"}]
|
||||
}]
|
||||
injectResponseHeaders: [
|
||||
{
|
||||
name: _AuthProxy.idTokenHeader
|
||||
values: [{claim: "id_token"}]
|
||||
},
|
||||
{
|
||||
name: "x-auth-request-email"
|
||||
values: [{claim: "email"}]
|
||||
},
|
||||
{
|
||||
name: "x-auth-request-groups"
|
||||
values: [{claim: "groups"}]
|
||||
},
|
||||
{
|
||||
name: "x-forwarded-email"
|
||||
values: [{claim: "email"}]
|
||||
},
|
||||
{
|
||||
name: "x-forwarded-user"
|
||||
values: [{claim: "email"}]
|
||||
},
|
||||
{
|
||||
name: "x-forwarded-preferred-username"
|
||||
values: [{claim: "preferred_username"}]
|
||||
},
|
||||
]
|
||||
providers: [{
|
||||
id: "Holos Platform"
|
||||
name: "Holos Platform"
|
||||
@@ -55,7 +77,7 @@ let Objects = {
|
||||
audienceClaims: ["aud"]
|
||||
emailClaim: "email"
|
||||
groupsClaim: "groups"
|
||||
userIDClaim: "sub"
|
||||
userIDClaim: "email"
|
||||
}
|
||||
}]
|
||||
server: BindAddress: ":\(_AuthProxy.servicePort)"
|
||||
|
||||
@@ -56,7 +56,7 @@ let Objects = {
|
||||
replicas: 2
|
||||
dataVolumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: "20Gi"
|
||||
resources: requests: storage: "50Gi"
|
||||
}
|
||||
}]
|
||||
standby: {
|
||||
@@ -122,7 +122,7 @@ let Objects = {
|
||||
name: "repo1"
|
||||
volume: volumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: string | *"4Gi"
|
||||
resources: requests: storage: string | *"50Gi"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -156,7 +156,7 @@ let HighlyAvailable = {
|
||||
replicas: 2
|
||||
dataVolumeClaimSpec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: string | *"20Gi"
|
||||
resources: requests: storage: string | *"50Gi"
|
||||
}
|
||||
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||
weight: 1
|
||||
|
||||
@@ -3,7 +3,7 @@ package holos
|
||||
#Values: {
|
||||
// https://github.com/zitadel/zitadel/releases
|
||||
// Overrides the image tag whose default is the chart appVersion.
|
||||
image: tag: "v2.49.1"
|
||||
image: tag: "v2.54.1"
|
||||
|
||||
// Database credentials
|
||||
// Refer to https://access.crunchydata.com/documentation/postgres-operator/5.2.0/architecture/user-management/
|
||||
|
||||
@@ -63,6 +63,29 @@ let FormBuilder = v1.#FormBuilder & {
|
||||
}
|
||||
}
|
||||
|
||||
Sections: github: {
|
||||
displayName: "GitHub"
|
||||
description: "Configure the platform GitHub integration. These values are used by the Backstage component."
|
||||
|
||||
fieldConfigs: {
|
||||
primaryOrg: {
|
||||
type: "input"
|
||||
props: {
|
||||
label: "Organization"
|
||||
description: "Primary GitHub orgranization where code repositories reside. \(validation.messages.required)"
|
||||
pattern: "^[a-z]([a-z0-9]|-){\(minLength-2),\(maxLength-2)}[a-z]$"
|
||||
minLength: 2
|
||||
maxLength: 39
|
||||
required: true
|
||||
}
|
||||
validation: messages: {
|
||||
pattern: "It must be \(props.minLength) to \(props.maxLength) lowercase letters, digits, or hyphens. It must start with a letter. Trailing hyphens are prohibited. \(required)"
|
||||
required: "GitHub organization name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sections: eso: {
|
||||
displayName: "Secret Store"
|
||||
description: "Configure the platform secret store. These values are used by the external-secrets-creds component. Note: this information is not sufficient to read secrets. To read secrets, the credential refresher job requires the workload clusters to be configured as workload identity providers."
|
||||
@@ -307,6 +330,37 @@ let FormBuilder = v1.#FormBuilder & {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sections: aws: {
|
||||
displayName: "AWS"
|
||||
description: "Configure AWS settings."
|
||||
|
||||
fieldConfigs: {
|
||||
accountNumber: {
|
||||
type: "input"
|
||||
props: {
|
||||
label: "AWS Account Number"
|
||||
description: "AWS Account Number. \(validation.messages.required)"
|
||||
pattern: "^[0-9]+$"
|
||||
required: true
|
||||
}
|
||||
validation: messages: {
|
||||
pattern: "Must be a positive integer. \(required)"
|
||||
required: "aws sts get-caller-identity"
|
||||
}
|
||||
}
|
||||
primaryRegion: {
|
||||
type: "select"
|
||||
props: {
|
||||
label: "Primary Region"
|
||||
description: "Select the primary region for AWS resources."
|
||||
multiple: false
|
||||
options: AWSRegions
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#StandardFields: {
|
||||
|
||||
@@ -40,6 +40,13 @@ package holos
|
||||
"authorization",
|
||||
"path",
|
||||
_AuthProxy.idTokenHeader,
|
||||
"x-forwaded-access-token",
|
||||
// For Backstage oauth2-proxy auth provider
|
||||
"x-forwarded-email",
|
||||
"x-forwarded-user",
|
||||
"x-forwarded-preferred-username",
|
||||
"x-auth-request-email",
|
||||
"x-auth-request-groups",
|
||||
]
|
||||
includeAdditionalHeadersInCheck: "X-Auth-Request-Redirect": "%REQ(x-forwarded-proto)%://%REQ(:authority)%%REQ(:path)%%REQ(:query)%"
|
||||
includeRequestHeadersInCheck: [
|
||||
|
||||
@@ -29,6 +29,7 @@ _Projects: {
|
||||
// Admin projects accessible at *.admin.<cluster>.<org.domain>
|
||||
holos: spec: namespaces: "holos-system": _
|
||||
argocd: spec: namespaces: argocd: _
|
||||
backstage: spec: namespaces: backstage: _
|
||||
|
||||
// Sync secrets from the management cluster to workload clusters.
|
||||
"external-secrets": spec: namespaces: "external-secrets": _
|
||||
@@ -62,24 +63,45 @@ _Projects: {
|
||||
spec: commonName: "*." + Subdomain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manage certificates for admin services in workload clusters.
|
||||
for Cluster in _Fleets.workload.clusters {
|
||||
// Issue a wildcard cert for all admin interfaces. We need to verify this is
|
||||
// well-behaved with Istio and HTTP2.
|
||||
let Subdomain = "admin.\(Cluster.name).\(_Platform.Model.org.domain)"
|
||||
_Projects: holos: spec: {
|
||||
// Crossplane
|
||||
crossplane: spec: namespaces: {
|
||||
"aws-pod-identity": _
|
||||
"crossplane-system": _
|
||||
}
|
||||
|
||||
holosapp: spec: {
|
||||
namespaces: "dev-holos": _
|
||||
namespaces: "prod-holos": _
|
||||
namespaces: "jeff-holos": _
|
||||
|
||||
let Subdomain = "app.\(_Platform.Model.org.domain)"
|
||||
certificates: "\(Subdomain)": #IngressCertificate
|
||||
certificates: "any.\(Subdomain)": #IngressCertificate & {
|
||||
spec: commonName: "*." + Subdomain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue a dedicated cert for argocd. This may be removed if the wildcard
|
||||
// works with the Gateway API.
|
||||
let Name = "argocd.\(Subdomain)"
|
||||
_Projects: argocd: spec: certificates: "\(Name)": #IngressCertificate & {metadata: name: Name}
|
||||
// Manage per-cluster certificates for services in workload clusters.
|
||||
for Cluster in _Fleets.workload.clusters {
|
||||
// Issue a wildcard cert for all admin interfaces. We need to verify this is
|
||||
// well-behaved with Istio and HTTP2.
|
||||
let CertPair = #ClusterCertPair & {cluster: Cluster.name}
|
||||
_Projects: holos: spec: (CertPair & {name: "admin"}).spec
|
||||
|
||||
// Holos app certs
|
||||
_Projects: holosapp: spec: (CertPair & {name: "app"}).spec
|
||||
}
|
||||
|
||||
#ClusterCertPair: {
|
||||
name: string
|
||||
cluster: string
|
||||
let Subdomain = name + ".\(cluster).\(_Platform.Model.org.domain)"
|
||||
spec: certificates: (Subdomain): #IngressCertificate
|
||||
spec: certificates: "any.\(Subdomain)": #IngressCertificate & {
|
||||
spec: commonName: "*." + Subdomain
|
||||
}
|
||||
}
|
||||
|
||||
// Platform components to manage.
|
||||
@@ -118,6 +140,28 @@ _Platform: Components: {
|
||||
path: "components/login/zitadel-certs"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
// ECR Credentials (ecr-creds-<account-number>)
|
||||
"\(Cluster.name)/ecr-creds-manager": {
|
||||
path: "components/ecr-creds-manager"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/eks-pod-identity-webhook": {
|
||||
path: "components/eks-pod-identity-webhook"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/crossplane_crds": {
|
||||
path: "components/crossplane/crds"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/crossplane": {
|
||||
path: "components/crossplane/controller"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
// Backstage certs
|
||||
"\(Cluster.name)/backstage-certs": {
|
||||
path: "components/backstage/management/certs"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
}
|
||||
|
||||
// Components to manage on workload clusters.
|
||||
@@ -134,6 +178,14 @@ _Platform: Components: {
|
||||
path: "components/secretstores"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
// Secret ecr-creds-<aws-account-number> in each Namespace to pull images
|
||||
// from the private ECR registry.
|
||||
"\(Cluster.name)/ecr-creds-refresher": {
|
||||
path: "components/ecr-creds-refresher"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
// We use HTTPRoute from the Kubernetes Gateway API v1 instead of
|
||||
// VirtualService from the Istio Gateway API.
|
||||
"\(Cluster.name)/gateway-api": {
|
||||
path: "components/gateway-api"
|
||||
cluster: Cluster.name
|
||||
@@ -213,6 +265,34 @@ _Platform: Components: {
|
||||
path: "components/argo/creds"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
|
||||
// Holos server
|
||||
"\(Cluster.name)/apps/dev-holos-infra": {
|
||||
path: "apps/dev/holos/infra"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/apps/dev-holos-app": {
|
||||
path: "apps/dev/holos/app"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
|
||||
// Backstage
|
||||
"\(Cluster.name)/backstage-secrets": {
|
||||
path: "components/backstage/workload/secrets"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/backstage-database": {
|
||||
path: "components/backstage/workload/database"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/backstage-backend": {
|
||||
path: "components/backstage/workload/backend"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
"\(Cluster.name)/backstage-routes": {
|
||||
path: "components/backstage/workload/routes"
|
||||
cluster: Cluster.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,3 +346,8 @@ _AuthProxy: {
|
||||
// provider is the istio meshconfig extauthz provider of the authproxy
|
||||
provider: "default-gateway-authproxy"
|
||||
}
|
||||
|
||||
_BackupBucket: {
|
||||
metadata: name: _Platform.Model.zitadel.backupBucketName
|
||||
spec: region: _Platform.Model.zitadel.backupBucketRegion
|
||||
}
|
||||
|
||||
@@ -164,3 +164,34 @@ _Projects: #Projects
|
||||
// #Selector represents label selectors.
|
||||
#Selector: [string]: matchLabels: {[string]: string}
|
||||
_Selector: #Selector
|
||||
|
||||
// #AppInfo represents the data structure for an application deployed onto the
|
||||
// platform.
|
||||
#AppInfo: {
|
||||
metadata: {
|
||||
name: string
|
||||
namespace: string
|
||||
labels: {[string]: string}
|
||||
annotations: {[string]: string}
|
||||
}
|
||||
|
||||
spec: env: string
|
||||
spec: component: string
|
||||
|
||||
spec: region: hostname: string
|
||||
spec: global: hostname: string
|
||||
|
||||
spec: dns: segments: {
|
||||
env: [] | [string]
|
||||
name: [] | [string]
|
||||
cluster: [] | [string]
|
||||
domain: [] | [string]
|
||||
}
|
||||
|
||||
// The primary port for HTTPRoute
|
||||
spec: port: number
|
||||
|
||||
spec: selector: matchLabels: {[string]: string}
|
||||
|
||||
status: component: string
|
||||
}
|
||||
|
||||
@@ -10,23 +10,54 @@ import (
|
||||
"github.com/holos-run/holos/internal/errors"
|
||||
"github.com/holos-run/holos/internal/server/middleware/logger"
|
||||
"github.com/holos-run/holos/internal/util"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func Platform(ctx context.Context, pf *v1alpha1.Platform, stderr io.Writer) error {
|
||||
func Platform(ctx context.Context, concurrency int, pf *v1alpha1.Platform, stderr io.Writer) error {
|
||||
total := len(pf.Spec.Components)
|
||||
for idx, component := range pf.Spec.Components {
|
||||
start := time.Now()
|
||||
log := logger.FromContext(ctx).With("path", component.Path, "cluster", component.Cluster, "num", idx+1, "total", total)
|
||||
log.DebugContext(ctx, "render component")
|
||||
// Execute a sub-process to limit CUE memory usage.
|
||||
args := []string{"render", "component", "--cluster-name", component.Cluster, component.Path}
|
||||
result, err := util.RunCmd(ctx, "holos", args...)
|
||||
if err != nil {
|
||||
_, _ = io.Copy(stderr, result.Stderr)
|
||||
return errors.Wrap(fmt.Errorf("could not render component: %w", err))
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
// Limit the number of concurrent goroutines due to CUE memory usage concerns
|
||||
// while rendering components. One more for the producer.
|
||||
g.SetLimit(concurrency + 1)
|
||||
|
||||
// Spawn a producer because g.Go() blocks when the group limit is reached.
|
||||
g.Go(func() error {
|
||||
for idx, component := range pf.Spec.Components {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// Capture idx and component to avoid issues with closure. Can be removed on Go 1.22.
|
||||
idx, component := idx, component
|
||||
// Worker go routine. Blocks if limit has been reached.
|
||||
g.Go(func() error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
start := time.Now()
|
||||
log := logger.FromContext(ctx).With("path", component.Path, "cluster", component.Cluster, "num", idx+1, "total", total)
|
||||
log.DebugContext(ctx, "render component")
|
||||
|
||||
// Execute a sub-process to limit CUE memory usage.
|
||||
args := []string{"render", "component", "--cluster-name", component.Cluster, component.Path}
|
||||
result, err := util.RunCmd(ctx, "holos", args...)
|
||||
if err != nil {
|
||||
_, _ = io.Copy(stderr, result.Stderr)
|
||||
return errors.Wrap(fmt.Errorf("could not render component: %w", err))
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
log.InfoContext(ctx, "ok render component", "duration", duration)
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
duration := time.Since(start)
|
||||
log.InfoContext(ctx, "ok render component", "duration", duration)
|
||||
}
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
|
||||
// Wait for completion and return the first error (if any)
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[s
|
||||
jeff, err := tx.User.Create().
|
||||
SetID(uuid.FromStringOrNil("018f36fb-e3f2-7f7f-a72f-ce48eb16c82d")).
|
||||
SetEmail("jeff@openinfrastructure.co").
|
||||
SetIss("https://login.ois.run").
|
||||
SetSub("261773693724656988").
|
||||
SetIss("https://login.holos.run").
|
||||
SetSub("270167759587952539").
|
||||
SetName("Jeff McCune").
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
@@ -111,8 +111,8 @@ func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[s
|
||||
}
|
||||
nate, err := tx.User.Create().
|
||||
SetEmail("nate@openinfrastructure.co").
|
||||
SetIss("https://login.ois.run").
|
||||
SetSub("261775487611699776").
|
||||
SetIss("https://login.holos.run").
|
||||
SetSub("269610170491914136").
|
||||
SetName("Nate McCurdy").
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
@@ -120,8 +120,8 @@ func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[s
|
||||
}
|
||||
gary, err := tx.User.Create().
|
||||
SetEmail("gary@openinfrastructure.co").
|
||||
SetIss("https://login.ois.run").
|
||||
SetSub("261775531836441152").
|
||||
SetIss("https://login.holos.run").
|
||||
SetSub("270168639938807707").
|
||||
SetName("Gary Larizza").
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
@@ -160,6 +160,21 @@ func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[s
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// The holos platform
|
||||
err = tx.Platform.Create().
|
||||
SetID(uuid.FromStringOrNil("018fa1cf-a609-7463-aa6e-fa53bfded1dc")).
|
||||
SetName("holos").
|
||||
SetDisplayName("Holos Platform").
|
||||
SetForm(&form).
|
||||
SetModel(&model).
|
||||
SetCreator(jeff).
|
||||
SetEditor(jeff).
|
||||
SetOrgID(org.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// Add a platform
|
||||
err = tx.Platform.Create().
|
||||
SetID(uuid.FromStringOrNil("018f36fb-e3ff-7f7f-a5d1-7ca2bf499e94")).
|
||||
@@ -175,22 +190,6 @@ func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[s
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
stuff := []string{"Jeff", "Gary", "Nate"}
|
||||
for _, name := range stuff {
|
||||
err := tx.Platform.Create().
|
||||
SetName(strings.ToLower(name)).
|
||||
SetDisplayName(name + "'s Platform").
|
||||
SetForm(&form).
|
||||
SetModel(&model).
|
||||
SetCreator(jeff).
|
||||
SetEditor(jeff).
|
||||
SetOrgID(org.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
|
||||
|
||||
@@ -66,10 +66,11 @@ type Claims struct {
|
||||
// NewConfig returns a Config with default values.
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Issuer: "https://login.ois.run",
|
||||
ClientID: "262479925313799528@holos_platform",
|
||||
Issuer: "https://login.holos.run",
|
||||
ClientID: "270319630705329162@holos_platform",
|
||||
Scopes: []string{"openid", "email", "profile", "groups", "offline_access"},
|
||||
ExtraScopes: []string{"urn:zitadel:iam:org:domain:primary:openinfrastructure.co"},
|
||||
ExtraScopes: []string{},
|
||||
// ExtraScopes: []string{"urn:zitadel:iam:org:domain:primary:openinfrastructure.co"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
83
|
||||
84
|
||||
|
||||
Reference in New Issue
Block a user