Add a rich example of fn framework for abstraction

This commit is contained in:
Katrina Verey
2023-04-06 20:04:55 -04:00
parent d3184da4c6
commit 85d623bc86
27 changed files with 1450 additions and 3 deletions

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eo pipefail
# This generates a useful starting point for a CRD that can be used for validation.
echo " - Generating CRD"
controller-gen crd paths=./... output:crd:dir=.
# controller-gen does not currently support "additionalProperties: false".
# This hack adds it manually to all properties sections of the schema.
echo " - Adding additionalProperties: false to all properties sections"
if [[ "$OSTYPE" == linux* ]]; then
# Linux (GNU sed) expression
sed -i "s/\(^[[:space:]]*\)properties:/\1additionalProperties: false\n\1properties:/g" ./platform.example.com_exampleapps.yaml
elif [[ "$OSTYPE" == darwin* ]]; then
# macOS (BSD sed) expression
sed -i "" "s/\(^[[:space:]]*\)properties:/\1additionalProperties: false\n\1properties:/g" ./platform.example.com_exampleapps.yaml
else
echo "Unsupported OS: $OSTYPE"
exit 1
fi

View File

@@ -0,0 +1,17 @@
package v1alpha1_test
import (
"testing"
"sigs.k8s.io/kustomize/functions/examples/fn-framework-application/pkg/dispatcher"
"sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil"
)
func TestExampleApp_GoldenFiles(t *testing.T) {
c := frameworktestutil.CommandResultsChecker{
Command: dispatcher.NewCommand,
// TestDataDirectory: "testdata/success",
// UpdateExpectedFromActual: true,
}
c.Assert(t)
}

View File

@@ -0,0 +1,131 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.11.3
creationTimestamp: null
name: exampleapps.platform.example.com
spec:
group: platform.example.com
names:
kind: ExampleApp
listKind: ExampleAppList
plural: exampleapps
singular: exampleapp
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
additionalProperties: false
properties:
apiVersion:
description: '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'
type: string
appImage:
type: string
datastores:
additionalProperties: false
properties:
postgresInstance:
type: string
type: object
env:
enum:
- production
- staging
- development
type: string
kind:
description: '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'
type: string
metadata:
type: object
overrides:
minProperties: 1
additionalProperties: false
properties:
additionalResources:
items:
type: string
type: array
containerPatches:
items:
type: string
type: array
resourcePatches:
items:
type: string
type: array
type: object
workloads:
minProperties: 1
additionalProperties: false
properties:
jobWorkers:
items:
additionalProperties: false
properties:
name:
pattern: ^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$
type: string
queues:
items:
type: string
minItems: 1
type: array
uniqueItems: true
replicas:
minimum: 0
type: integer
resources:
enum:
- small
- medium
- large
type: string
required:
- name
- queues
type: object
type: array
webWorkers:
items:
additionalProperties: false
properties:
domains:
items:
type: string
minItems: 1
type: array
uniqueItems: true
name:
pattern: ^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$
type: string
replicas:
minimum: 0
type: integer
resources:
enum:
- small
- medium
- large
type: string
required:
- domains
- name
type: object
type: array
type: object
required:
- env
- metadata
- workloads
type: object
served: true
storage: true

View File

@@ -0,0 +1,216 @@
package v1alpha1
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
//go:embed templates/*
var templateFS embed.FS
func (a *ExampleApp) Schema() (*spec.Schema, error) {
schema, err := framework.SchemaFromFunctionDefinition(resid.NewGvk(Group, Version, Kind), CRDString)
return schema, errors.WrapPrefixf(err, "parsing %s schema", Kind)
}
func (a *ExampleApp) Default() error {
if a.AppImage == "" {
a.AppImage = fmt.Sprintf("registry.example.com/path/to/%s", a.ObjectMeta.Name)
}
for i := range a.Workloads.WebWorkers {
if a.Workloads.WebWorkers[i].Replicas == nil {
a.Workloads.WebWorkers[i].Replicas = func() *int { three := 3; return &three }()
}
if a.Workloads.WebWorkers[i].Resources == "" {
a.Workloads.WebWorkers[i].Resources = ResourceBinSizeSmall
}
}
for i := range a.Workloads.JobWorkers {
if a.Workloads.JobWorkers[i].Replicas == nil {
a.Workloads.JobWorkers[i].Replicas = func() *int { one := 1; return &one }()
}
if a.Workloads.JobWorkers[i].Resources == "" {
a.Workloads.JobWorkers[i].Resources = ResourceBinSizeSmall
}
}
return nil
}
func (a *ExampleApp) Validate() error {
seenDomains := make(map[string]bool)
for i, worker := range a.Workloads.WebWorkers {
for _, domain := range worker.Domains {
if seenDomains[domain] {
return errors.Errorf("duplicate domain %q in worker %d", domain, i)
}
seenDomains[domain] = true
}
}
return nil
}
func (a ExampleApp) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
templates := make([]framework.ResourceTemplate, 0)
for _, worker := range a.Workloads.JobWorkers {
templates = append(templates, framework.ResourceTemplate{
Templates: parser.TemplateFiles("templates/job_worker.template.yaml").FromFS(templateFS),
TemplateData: a.jobWorkerTemplateData(worker),
})
}
for _, worker := range a.Workloads.WebWorkers {
templates = append(templates, framework.ResourceTemplate{
Templates: parser.TemplateFiles("templates/web_worker.template.yaml").FromFS(templateFS),
TemplateData: a.webWorkerTemplateData(worker),
})
}
var patches []framework.PatchTemplate
if a.Datastores.PostgresInstance != "" {
templates = append(templates, framework.ResourceTemplate{
TemplateData: map[string]interface{}{"Name": a.Datastores.PostgresInstance},
Templates: parser.TemplateStrings(`apiVersion: apps.example.com/v1
kind: PostgresSecretRequest
metadata:
name: {{ .Name }}
`)})
patches = append(patches, framework.PatchTemplate(&framework.ContainerPatchTemplate{
Templates: parser.TemplateFiles("templates/postgres_secret_env_patch.template.yaml").FromFS(templateFS),
ContainerMatcher: framework.ContainerNameMatcher("app"),
}))
}
if len(a.Overrides.AdditionalResources) > 0 {
templates = append(templates, framework.ResourceTemplate{
Templates: parser.TemplateFiles(a.Overrides.AdditionalResources...).WithExtensions(".yaml", ".template.yaml"),
TemplateData: a,
})
}
for i, resource := range a.Overrides.ResourcePatches {
overridePatches, err := a.resourceSMPsFromOverrides(resource, i, patches)
if err != nil {
return nil, err
}
patches = append(patches, overridePatches...)
}
if len(a.Overrides.ContainerPatches) > 0 {
patches = append(patches, framework.PatchTemplate(&framework.ContainerPatchTemplate{
Templates: parser.TemplateFiles(a.Overrides.ContainerPatches...).WithExtensions(".yaml", ".template.yaml"),
TemplateData: a,
}))
}
items, err := framework.TemplateProcessor{
ResourceTemplates: templates,
PatchTemplates: patches,
}.Filter(items)
if err != nil {
return nil, errors.WrapPrefixf(err, "processing templates")
}
return items, nil
}
// resourceSMPsFromOverrides parses the resource template and returns a patch that
// is targeted to match resources with the same GVKNN the patch itself contains.
// TODO: This is standard SMP semantics, so the framework should make this easier.
func (a ExampleApp) resourceSMPsFromOverrides(resource string, i int, patches []framework.PatchTemplate) ([]framework.PatchTemplate, error) {
tpl, err := parser.TemplateFiles(resource).WithExtensions(".yaml", ".template.yaml").Parse()
if err != nil {
return nil, errors.WrapPrefixf(err, "parsing resource template %d", i)
}
for _, template := range tpl {
var b bytes.Buffer
if err := template.Execute(&b, a); err != nil {
return nil, errors.WrapPrefixf(err, "failed to render patch template %v", template.DefinedTemplates())
}
var id yaml.ResourceMeta
err := yaml.Unmarshal(b.Bytes(), &id)
if err != nil {
return nil, errors.WrapPrefixf(err, "failed to unmarshal resource identifier from %v", template.DefinedTemplates())
}
selector := framework.MatchAll(
framework.GVKMatcher(strings.Join([]string{id.APIVersion, id.Kind}, "/")), framework.NameMatcher(id.Name),
framework.NamespaceMatcher(id.Namespace))
selector.FailOnEmptyMatch = true
patches = append(patches, framework.PatchTemplate(&framework.ResourcePatchTemplate{
Templates: parser.TemplateFiles(a.Overrides.ResourcePatches...).WithExtensions(".yaml", ".template.yaml"),
Selector: selector,
}))
}
return patches, nil
}
type resourceBucket struct {
Requests resourceAllocation `yaml:"requests" json:"requests"`
Limits resourceAllocation `yaml:"limits" json:"limits"`
}
type resourceAllocation struct {
CPU string `yaml:"cpu" json:"cpu"`
Memory string `yaml:"memory" json:"memory"`
}
//nolint:gochecknoglobals
var resourceBucketConversion = map[ResourceBinSize]resourceBucket{
"small": {
Requests: resourceAllocation{CPU: "100m", Memory: "128Mi"},
Limits: resourceAllocation{CPU: "500m", Memory: "512Mi"},
},
"medium": {
Requests: resourceAllocation{CPU: "1", Memory: "1Gi"},
Limits: resourceAllocation{CPU: "2", Memory: "2Gi"},
},
"large": {
Requests: resourceAllocation{CPU: "8", Memory: "8Gi"},
Limits: resourceAllocation{CPU: "8", Memory: "8Gi"},
},
}
const anArbitraryMultiplier = 2
func (a ExampleApp) jobWorkerTemplateData(w JobWorker) map[string]interface{} {
resourcesJson, err := json.Marshal(resourceBucketConversion[w.Resources])
if err != nil {
panic("failed to marshal resources for job worker" + err.Error())
}
return map[string]interface{}{
"Name": w.Name,
"AppImage": a.AppImage,
"QueueList": strings.Join(w.Queues, ","),
"ProcessPoolSize": len(w.Queues) * anArbitraryMultiplier,
"Resources": string(resourcesJson),
"Replicas": w.Replicas,
"Environment": a.Env,
}
}
const containerPort = 8080
func (a ExampleApp) webWorkerTemplateData(w WebWorker) map[string]interface{} {
resourcesJson, err := json.Marshal(resourceBucketConversion[w.Resources])
if err != nil {
panic("failed to marshal resources for web worker" + err.Error())
}
return map[string]interface{}{
"Name": w.Name,
"AppImage": a.AppImage,
"Resources": string(resourcesJson),
"Replicas": w.Replicas,
"Port": containerPort,
"Environment": a.Env,
}
}

View File

@@ -0,0 +1,45 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
name: {{ .Name }}
env: {{ .Environment }}
type: jobs
name: {{ .Name }}
spec:
replicas: {{ .Replicas }}
selector:
matchLabels:
type: jobs
name: {{ .Name }}
env: {{ .Environment }}
template:
metadata:
labels:
type: jobs
name: {{ .Name }}
env: {{ .Environment }}
spec:
automountServiceAccountToken: false
containers:
- name: app
image: {{ .AppImage }}
args: [
"job-worker",
"--queues",
"{{ .QueueList }}",
"--workers", "{{ .ProcessPoolSize }}",
]
readinessProbe:
exec:
command:
- "bin/job-worker-readiness-probe"
resources: {{ .Resources }}
env:
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ENV
value: {{ .Environment }}

View File

@@ -0,0 +1,6 @@
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: DATABASE_URL

View File

@@ -0,0 +1,50 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Name }}
labels:
name: {{ .Name }}
env: {{ .Environment }}
type: web
spec:
replicas: {{ .Replicas }}
selector:
matchLabels:
name: {{ .Name }}
env: {{ .Environment }}
type: web
template:
metadata:
labels:
name: {{ .Name }}
env: {{ .Environment }}
type: web
spec:
containers:
- name: app
image: {{ .AppImage }}
args:
- web
ports:
- containerPort: {{ .Port }}
name: http
readinessProbe:
httpGet:
path: "/ping"
port: {{ .Port }}
httpHeaders:
- name: "X-Forwarded-Proto"
value: "https"
initialDelaySeconds: 20
timeoutSeconds: 3
env:
- name: ENV
value: {{ .Environment }}
- name: PORT
value: "{{ .Port }}"
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources: {{ .Resources }}

View File

@@ -0,0 +1,21 @@
apiVersion: platform.example.com/v1alpha1
kind: ExampleApp
metadata:
name: simple-app-sample
env: invalid
workloads:
webWorkers:
- name: web-worker
replicas: -1
resources: small
domains:
- example.com
- name: web-worker-2
domains:
- example.com
jobWorkers:
- name: job-worker-$$
queues: []
datastores:
mongoDB: simple-app-sample-mongo
extraProperty: true

View File

@@ -0,0 +1,8 @@
validation failure list:
.extraProperty in body is a forbidden property
workloads.jobWorkers\[0\].name in body should match
workloads.jobWorkers\[0\].queues in body should have at least 1 items
workloads.webWorkers\[0\].replicas in body should be greater than or equal to 0
datastores.mongoDB in body is a forbidden property
env in body should be one of \[production staging development\]
duplicate domain \"example.com\" in worker 1

View File

@@ -0,0 +1,24 @@
apiVersion: platform.example.com/v1alpha1
kind: ExampleApp
metadata:
name: simple-app-sample
env: production
workloads:
webWorkers:
- name: web-worker
domains:
- example.com
jobWorkers:
- name: job-worker
replicas: 10
resources: medium
queues:
- high
- medium
- low
- name: job-worker-2
replicas: 5
queues:
- bg2
datastores:
postgresInstance: simple-app-sample-postgres

View File

@@ -0,0 +1,149 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: job-worker
labels:
name: job-worker
type: jobs
env: production
spec:
replicas: 10
selector:
matchLabels:
name: job-worker
type: jobs
env: production
template:
metadata:
labels:
name: job-worker
type: jobs
env: production
spec:
automountServiceAccountToken: false
containers:
- name: app
image: registry.example.com/path/to/simple-app-sample
args: ["job-worker", "--queues", "high,medium,low", "--workers", "6"]
env:
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: DATABASE_URL
resources: {"limits": {"cpu": "2", "memory": "2Gi"}, "requests": {"cpu": "1", "memory": "1Gi"}}
readinessProbe:
exec:
command:
- "bin/job-worker-readiness-probe"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: job-worker-2
labels:
name: job-worker-2
type: jobs
env: production
spec:
replicas: 5
selector:
matchLabels:
name: job-worker-2
type: jobs
env: production
template:
metadata:
labels:
name: job-worker-2
type: jobs
env: production
spec:
automountServiceAccountToken: false
containers:
- name: app
image: registry.example.com/path/to/simple-app-sample
args: ["job-worker", "--queues", "bg2", "--workers", "2"]
env:
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: DATABASE_URL
resources: {"limits": {"cpu": "500m", "memory": "512Mi"}, "requests": {"cpu": "100m", "memory": "128Mi"}}
readinessProbe:
exec:
command:
- "bin/job-worker-readiness-probe"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-worker
labels:
name: web-worker
type: web
env: production
spec:
replicas: 3
selector:
matchLabels:
name: web-worker
type: web
env: production
template:
metadata:
labels:
name: web-worker
type: web
env: production
spec:
containers:
- name: app
image: registry.example.com/path/to/simple-app-sample
args:
- web
ports:
- name: http
containerPort: 8080
env:
- name: ENV
value: production
- name: PORT
value: "8080"
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: DATABASE_URL
resources: {"limits": {"cpu": "500m", "memory": "512Mi"}, "requests": {"cpu": "100m", "memory": "128Mi"}}
readinessProbe:
httpGet:
port: 8080
httpHeaders:
- name: "X-Forwarded-Proto"
value: "https"
path: "/ping"
initialDelaySeconds: 20
timeoutSeconds: 3
---
apiVersion: apps.example.com/v1
kind: PostgresSecretRequest
metadata:
name: simple-app-sample-postgres

View File

@@ -0,0 +1,21 @@
apiVersion: platform.example.com/v1alpha1
kind: ExampleApp
metadata:
name: simple-app-sample
env: production
workloads:
webWorkers:
- name: web-worker
domains:
- first.example.com
- name: web-worker-no-sidecar
domains:
- second.example.com
overrides:
additionalResources:
- custom-configmap.yaml
resourcePatches:
- web-worker-sidecar.yaml
containerPatches:
- custom-app-env.yaml

View File

@@ -0,0 +1,6 @@
env:
- name: MY_NEW_VAR
value: "new value"
envFrom:
- configMapRef:
name: custom-configmap

View File

@@ -0,0 +1,6 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: custom-configmap
data:
LOADED: "true"

View File

@@ -0,0 +1,126 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-worker
labels:
name: web-worker
type: web
env: production
spec:
replicas: 3
selector:
matchLabels:
name: web-worker
type: web
env: production
template:
metadata:
labels:
name: web-worker
type: web
env: production
spec:
containers:
- name: app
image: registry.example.com/path/to/simple-app-sample
args:
- web
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: custom-configmap
env:
- name: ENV
value: production
- name: PORT
value: "8080"
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_NEW_VAR
value: "new value"
resources: {"limits": {"cpu": "500m", "memory": "512Mi"}, "requests": {"cpu": "100m", "memory": "128Mi"}}
readinessProbe:
httpGet:
port: 8080
httpHeaders:
- name: "X-Forwarded-Proto"
value: "https"
path: "/ping"
initialDelaySeconds: 20
timeoutSeconds: 3
- name: sidecar
image: registry.example.com/path/to/custom-sidecar
args:
- run
envFrom:
- configMapRef:
name: custom-configmap
env:
- name: MY_NEW_VAR
value: "new value"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-worker-no-sidecar
labels:
name: web-worker-no-sidecar
type: web
env: production
spec:
replicas: 3
selector:
matchLabels:
name: web-worker-no-sidecar
type: web
env: production
template:
metadata:
labels:
name: web-worker-no-sidecar
type: web
env: production
spec:
containers:
- name: app
image: registry.example.com/path/to/simple-app-sample
args:
- web
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: custom-configmap
env:
- name: ENV
value: production
- name: PORT
value: "8080"
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_NEW_VAR
value: "new value"
resources: {"limits": {"cpu": "500m", "memory": "512Mi"}, "requests": {"cpu": "100m", "memory": "128Mi"}}
readinessProbe:
httpGet:
port: 8080
httpHeaders:
- name: "X-Forwarded-Proto"
value: "https"
path: "/ping"
initialDelaySeconds: 20
timeoutSeconds: 3
---
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-configmap
data:
LOADED: "true"

View File

@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-worker
spec:
template:
spec:
containers:
- name: sidecar
image: registry.example.com/path/to/custom-sidecar
args:
- run

View File

@@ -0,0 +1,108 @@
// +groupName=platform.example.com
// +versionName=v1alpha1
// +kubebuilder:validation:Required
package v1alpha1
import (
_ "embed"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
//go:generate ./crd_gen.sh
//go:embed platform.example.com_exampleapps.yaml
var CRDString string
const Group = "platform.example.com"
const Version = "v1alpha1"
const Kind = "ExampleApp"
//nolint:gochecknoglobals
var GroupVersion = strings.Join([]string{Group, Version}, "/")
type ExampleApp struct {
// Embedding these structs is required to use controller-gen to produce the CRD
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
// +kubebuilder:validation:Enum=production;staging;development
Env string `json:"env" yaml:"env"`
// +optional
AppImage string `json:"appImage" yaml:"appImage"`
Workloads Workloads `json:"workloads" yaml:"workloads"`
// +optional
Datastores Datastores `json:"datastores,omitempty" yaml:"datastores,omitempty"`
// +optional
Overrides Overrides `json:"overrides,omitempty" yaml:"overrides,omitempty"`
}
// +kubebuilder:validation:MinProperties=1
type Overrides struct {
// +optional
AdditionalResources []string `json:"additionalResources,omitempty" yaml:"additionalResources,omitempty"`
// +optional
ResourcePatches []string `json:"resourcePatches,omitempty" yaml:"resourcePatches,omitempty"`
// +optional
ContainerPatches []string `json:"containerPatches,omitempty" yaml:"containerPatches,omitempty"`
}
// +kubebuilder:validation:MinProperties=1
type Workloads struct {
// +optional
JobWorkers []JobWorker `json:"jobWorkers,omitempty" yaml:"jobWorkers,omitempty"`
// +optional
WebWorkers []WebWorker `json:"webWorkers,omitempty" yaml:"webWorkers,omitempty"`
}
// +kubebuilder:validation:Pattern="^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$"
type KubernetesMetaName string
type JobWorker struct {
Name KubernetesMetaName `json:"name" yaml:"name"`
// +kubebuilder:validation:Minimum=0
// +optional
Replicas *int `json:"replicas,omitempty" yaml:"replicas,omitempty"`
// +optional
Resources ResourceBinSize `json:"resources,omitempty" yaml:"resources,omitempty"`
// +kubebuilder:validation:UniqueItems=true
// +kubebuilder:validation:MinItems=1
Queues []string `json:"queues" yaml:"queues"`
}
// +kubebuilder:validation:Enum=small;medium;large
type ResourceBinSize string
const ResourceBinSizeSmall ResourceBinSize = "small"
type WebWorker struct {
Name KubernetesMetaName `json:"name" yaml:"name"`
// +kubebuilder:validation:Minimum=0
// +optional
Replicas *int `json:"replicas,omitempty" yaml:"replicas,omitempty"`
// +optional
Resources ResourceBinSize `json:"resources,omitempty" yaml:"resources,omitempty"`
// +kubebuilder:validation:UniqueItems=true
// +kubebuilder:validation:MinItems=1
Domains []string `json:"domains" yaml:"domains"`
}
type Datastores struct {
// +optional
PostgresInstance string `json:"postgresInstance,omitempty" yaml:"postgresInstance,omitempty"`
}