feat(dashboard): initial support for studio & pg-meta services
This commit is contained in:
parent
7d9e518f86
commit
0b551325b9
31 changed files with 2151 additions and 492 deletions
4
PROJECT
4
PROJECT
|
@ -43,4 +43,8 @@ resources:
|
|||
kind: Dashboard
|
||||
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
webhooks:
|
||||
defaulting: true
|
||||
validation: true
|
||||
webhookVersion: v1
|
||||
version: "3"
|
||||
|
|
11
Tiltfile
11
Tiltfile
|
@ -67,3 +67,14 @@ k8s_resource(
|
|||
'supabase-controller-manager'
|
||||
],
|
||||
)
|
||||
|
||||
k8s_resource(
|
||||
objects=["core-sample:Dashboard:supabase-demo"],
|
||||
extra_pod_selectors={"app.kubernetes.io/component": "dashboard", "app.kubernetes.io/name": "studio"},
|
||||
discovery_strategy="selectors-only",
|
||||
port_forwards=[3000],
|
||||
new_name='Dashboard',
|
||||
resource_deps=[
|
||||
'supabase-controller-manager'
|
||||
],
|
||||
)
|
||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
|
@ -42,3 +44,133 @@ type WorkloadTemplate struct {
|
|||
// Workload - customize the container template of the workload
|
||||
Workload *ContainerTemplate `json:"workload,omitempty"`
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) ReplicaCount() *int32 {
|
||||
if t != nil && t.Replicas != nil {
|
||||
return t.Replicas
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) MergeEnv(basicEnv []corev1.EnvVar) []corev1.EnvVar {
|
||||
if t == nil || t.Workload == nil || len(t.Workload.AdditionalEnv) == 0 {
|
||||
return basicEnv
|
||||
}
|
||||
|
||||
existingKeys := make(map[string]bool, len(basicEnv)+len(t.Workload.AdditionalEnv))
|
||||
|
||||
merged := append(make([]corev1.EnvVar, 0, len(basicEnv)+len(t.Workload.AdditionalEnv)), basicEnv...)
|
||||
|
||||
for _, v := range basicEnv {
|
||||
existingKeys[v.Name] = true
|
||||
}
|
||||
|
||||
for _, v := range t.Workload.AdditionalEnv {
|
||||
if _, alreadyPresent := existingKeys[v.Name]; alreadyPresent {
|
||||
continue
|
||||
}
|
||||
merged = append(merged, v)
|
||||
existingKeys[v.Name] = true
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) MergeLabels(initial map[string]string, toAppend ...map[string]string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
maps.Copy(result, initial)
|
||||
|
||||
var labelSets []map[string]string
|
||||
|
||||
if t != nil && len(t.AdditionalLabels) > 0 {
|
||||
labelSets = append(labelSets, t.AdditionalLabels)
|
||||
}
|
||||
|
||||
labelSets = append(labelSets, toAppend...)
|
||||
|
||||
for _, lbls := range labelSets {
|
||||
for k, v := range lbls {
|
||||
if _, ok := result[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) Image(defaultImage string) string {
|
||||
if t != nil && t.Workload != nil && t.Workload.Image != "" {
|
||||
return t.Workload.Image
|
||||
}
|
||||
|
||||
return defaultImage
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) ImagePullPolicy() corev1.PullPolicy {
|
||||
if t != nil && t.Workload != nil && t.Workload.PullPolicy != "" {
|
||||
return t.Workload.PullPolicy
|
||||
}
|
||||
|
||||
return corev1.PullIfNotPresent
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) PullSecrets() []corev1.LocalObjectReference {
|
||||
if t != nil && t.Workload != nil && len(t.Workload.ImagePullSecrets) > 0 {
|
||||
return t.Workload.ImagePullSecrets
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) Resources() corev1.ResourceRequirements {
|
||||
if t != nil && t.Workload != nil {
|
||||
return t.Workload.Resources
|
||||
}
|
||||
|
||||
return corev1.ResourceRequirements{}
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) AdditionalVolumeMounts(defaultMounts ...corev1.VolumeMount) []corev1.VolumeMount {
|
||||
if t != nil && t.Workload != nil {
|
||||
return append(defaultMounts, t.Workload.VolumeMounts...)
|
||||
}
|
||||
|
||||
return defaultMounts
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) PodSecurityContext() *corev1.PodSecurityContext {
|
||||
if t != nil && t.SecurityContext != nil {
|
||||
return t.SecurityContext
|
||||
}
|
||||
|
||||
return &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WorkloadTemplate) ContainerSecurityContext(uid, gid int64) *corev1.SecurityContext {
|
||||
if t != nil && t.Workload != nil && t.Workload.SecurityContext != nil {
|
||||
return t.Workload.SecurityContext
|
||||
}
|
||||
|
||||
return &corev1.SecurityContext{
|
||||
Privileged: ptrOf(false),
|
||||
RunAsUser: ptrOf(uid),
|
||||
RunAsGroup: ptrOf(gid),
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
AllowPrivilegeEscalation: ptrOf(false),
|
||||
ReadOnlyRootFilesystem: ptrOf(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ptrOf[T any](val T) *T {
|
||||
return &val
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ func (d Database) DSNEnv(key string) corev1.EnvVar {
|
|||
}
|
||||
}
|
||||
|
||||
type JwtSpec struct {
|
||||
type CoreJwtSpec struct {
|
||||
// Secret - JWT HMAC secret in plain text
|
||||
// This is WRITE-ONLY and will be copied to the SecretRef by the defaulter
|
||||
Secret *string `json:"secret,omitempty"`
|
||||
|
@ -113,7 +113,7 @@ type JwtSpec struct {
|
|||
Expiry int `json:"expiry,omitempty"`
|
||||
}
|
||||
|
||||
func (s JwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]byte, error) {
|
||||
func (s CoreJwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]byte, error) {
|
||||
var secret corev1.Secret
|
||||
if err := client.Get(ctx, types.NamespacedName{Name: s.SecretRef.Name}, &secret); err != nil {
|
||||
return nil, nil
|
||||
|
@ -127,21 +127,21 @@ func (s JwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]byte
|
|||
return value, nil
|
||||
}
|
||||
|
||||
func (s JwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
||||
func (s CoreJwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
||||
return &corev1.SecretKeySelector{
|
||||
LocalObjectReference: *s.SecretRef,
|
||||
Key: s.SecretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s JwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
|
||||
func (s CoreJwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
|
||||
return &corev1.SecretKeySelector{
|
||||
LocalObjectReference: *s.SecretRef,
|
||||
Key: s.JwksKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s JwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
||||
func (s CoreJwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: key,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
|
@ -153,7 +153,7 @@ func (s JwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
|||
}
|
||||
}
|
||||
|
||||
func (s JwtSpec) ExpiryAsEnv(key string) corev1.EnvVar {
|
||||
func (s CoreJwtSpec) ExpiryAsEnv(key string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: key,
|
||||
Value: strconv.Itoa(s.Expiry),
|
||||
|
@ -358,12 +358,6 @@ func (p *AuthProviders) Vars(apiExternalURL string) []corev1.EnvVar {
|
|||
}
|
||||
|
||||
type AuthSpec struct {
|
||||
// APIExternalURL is referring to the URL where Supabase API will be available
|
||||
// Typically this is the ingress of the API gateway
|
||||
APIExternalURL string `json:"externalUrl"`
|
||||
// SiteURL is referring to the URL of the (frontend) application
|
||||
// In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress
|
||||
SiteURL string `json:"siteUrl"`
|
||||
AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"`
|
||||
DisableSignup *bool `json:"disableSignup,omitempty"`
|
||||
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
|
||||
|
@ -374,7 +368,13 @@ type AuthSpec struct {
|
|||
|
||||
// CoreSpec defines the desired state of Core.
|
||||
type CoreSpec struct {
|
||||
JWT *JwtSpec `json:"jwt,omitempty"`
|
||||
// APIExternalURL is referring to the URL where Supabase API will be available
|
||||
// Typically this is the ingress of the API gateway
|
||||
APIExternalURL string `json:"externalUrl"`
|
||||
// SiteURL is referring to the URL of the (frontend) application
|
||||
// In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress
|
||||
SiteURL string `json:"siteUrl"`
|
||||
JWT *CoreJwtSpec `json:"jwt,omitempty"`
|
||||
Database Database `json:"database,omitempty"`
|
||||
Postgrest PostgrestSpec `json:"postgrest,omitempty"`
|
||||
Auth *AuthSpec `json:"auth,omitempty"`
|
||||
|
|
|
@ -21,9 +21,53 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type DashboardJwtSpec struct {
|
||||
// SecretRef - object reference to the Secret where JWT values are stored
|
||||
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
|
||||
// SecretKey - key in secret where to read the JWT HMAC secret from
|
||||
// +kubebuilder:default=secret
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
// AnonKey - key in secret where to read the anon JWT from
|
||||
// +kubebuilder:default=anon_key
|
||||
AnonKey string `json:"anonKey,omitempty"`
|
||||
// ServiceKey - key in secret where to read the service JWT from
|
||||
// +kubebuilder:default=service_key
|
||||
ServiceKey string `json:"serviceKey,omitempty"`
|
||||
}
|
||||
|
||||
func (s DashboardJwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
||||
return &corev1.SecretKeySelector{
|
||||
LocalObjectReference: *s.SecretRef,
|
||||
Key: s.SecretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s DashboardJwtSpec) AnonKeySelector() *corev1.SecretKeySelector {
|
||||
return &corev1.SecretKeySelector{
|
||||
LocalObjectReference: *s.SecretRef,
|
||||
Key: s.AnonKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s DashboardJwtSpec) ServiceKeySelector() *corev1.SecretKeySelector {
|
||||
return &corev1.SecretKeySelector{
|
||||
LocalObjectReference: *s.SecretRef,
|
||||
Key: s.ServiceKey,
|
||||
}
|
||||
}
|
||||
|
||||
type StudioSpec struct {
|
||||
JWT *DashboardJwtSpec `json:"jwt,omitempty"`
|
||||
// WorkloadTemplate - customize the studio deployment
|
||||
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
||||
// GatewayServiceSelector - selector to find the service for the API gateway
|
||||
// Required to configure the API URL in the studio deployment
|
||||
// If you don't run multiple APIGateway instances in the same namespaces, the default will be fine
|
||||
// +kubebuilder:default={"app.kubernetes.io/name":"envoy","app.kubernetes.io/component":"api-gateway"}
|
||||
GatewayServiceMatchLabels map[string]string `json:"gatewayServiceSelector,omitempty"`
|
||||
// APIExternalURL is referring to the URL where Supabase API will be available
|
||||
// Typically this is the ingress of the API gateway
|
||||
APIExternalURL string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
type PGMetaSpec struct {
|
||||
|
@ -60,10 +104,8 @@ func (s DashboardDbSpec) PasswordRef() *corev1.SecretKeySelector {
|
|||
type DashboardSpec struct {
|
||||
DBSpec *DashboardDbSpec `json:"db"`
|
||||
// PGMeta
|
||||
// +kubebuilder:default={}
|
||||
PGMeta *PGMetaSpec `json:"pgMeta,omitempty"`
|
||||
// Studio
|
||||
// +kubebuilder:default={}
|
||||
Studio *StudioSpec `json:"studio,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -319,6 +319,31 @@ func (in *Core) DeepCopyObject() runtime.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CoreJwtSpec) DeepCopyInto(out *CoreJwtSpec) {
|
||||
*out = *in
|
||||
if in.Secret != nil {
|
||||
in, out := &in.Secret, &out.Secret
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreJwtSpec.
|
||||
func (in *CoreJwtSpec) DeepCopy() *CoreJwtSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CoreJwtSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CoreList) DeepCopyInto(out *CoreList) {
|
||||
*out = *in
|
||||
|
@ -356,7 +381,7 @@ func (in *CoreSpec) DeepCopyInto(out *CoreSpec) {
|
|||
*out = *in
|
||||
if in.JWT != nil {
|
||||
in, out := &in.JWT, &out.JWT
|
||||
*out = new(JwtSpec)
|
||||
*out = new(CoreJwtSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.Database.DeepCopyInto(&out.Database)
|
||||
|
@ -441,6 +466,26 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DashboardJwtSpec) DeepCopyInto(out *DashboardJwtSpec) {
|
||||
*out = *in
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardJwtSpec.
|
||||
func (in *DashboardJwtSpec) DeepCopy() *DashboardJwtSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DashboardJwtSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DashboardList) DeepCopyInto(out *DashboardList) {
|
||||
*out = *in
|
||||
|
@ -751,31 +796,6 @@ func (in *ImageSpec) DeepCopy() *ImageSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JwtSpec) DeepCopyInto(out *JwtSpec) {
|
||||
*out = *in
|
||||
if in.Secret != nil {
|
||||
in, out := &in.Secret, &out.Secret
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JwtSpec.
|
||||
func (in *JwtSpec) DeepCopy() *JwtSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JwtSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in MigrationStatus) DeepCopyInto(out *MigrationStatus) {
|
||||
{
|
||||
|
@ -886,11 +906,23 @@ func (in *PostgrestSpec) DeepCopy() *PostgrestSpec {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
|
||||
*out = *in
|
||||
if in.JWT != nil {
|
||||
in, out := &in.JWT, &out.JWT
|
||||
*out = new(DashboardJwtSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.WorkloadTemplate != nil {
|
||||
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||
*out = new(WorkloadTemplate)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.GatewayServiceMatchLabels != nil {
|
||||
in, out := &in.GatewayServiceMatchLabels, &out.GatewayServiceMatchLabels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StudioSpec.
|
||||
|
|
|
@ -144,25 +144,36 @@ func (m manager) Run(ctx context.Context) error {
|
|||
return fmt.Errorf("unable to create controller Dashboard PG-Meta: %w", err)
|
||||
}
|
||||
|
||||
// nolint:goconst
|
||||
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
||||
if err = webhooksupabasev1alpha1.SetupCoreWebhookWithManager(mgr); err != nil {
|
||||
return fmt.Errorf("unable to create webhook: %w", err)
|
||||
}
|
||||
if err = (&controller.DashboardStudioReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
return fmt.Errorf("unable to create controller Dashboard PG-Meta: %w", err)
|
||||
}
|
||||
|
||||
if err = (&controller.APIGatewayReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(ctx, mgr); err != nil {
|
||||
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
||||
}
|
||||
|
||||
// nolint:goconst
|
||||
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
||||
if err = webhooksupabasev1alpha1.SetupCoreWebhookWithManager(mgr); err != nil {
|
||||
return fmt.Errorf("unable to create webhook: %w", err)
|
||||
}
|
||||
|
||||
if err = webhooksupabasev1alpha1.SetupAPIGatewayWebhookWithManager(mgr, webhookConfig); err != nil {
|
||||
setupLog.Error(err, "unable to create webhook", "webhook", "APIGateway")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = webhooksupabasev1alpha1.SetupDashboardWebhookWithManager(mgr); err != nil {
|
||||
return fmt.Errorf("unable to create webhook: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
|
|
@ -51,11 +51,6 @@ spec:
|
|||
type: boolean
|
||||
emailSignupDisabled:
|
||||
type: boolean
|
||||
externalUrl:
|
||||
description: |-
|
||||
APIExternalURL is referring to the URL where Supabase API will be available
|
||||
Typically this is the ingress of the API gateway
|
||||
type: string
|
||||
providers:
|
||||
properties:
|
||||
azure:
|
||||
|
@ -191,11 +186,6 @@ spec:
|
|||
type: boolean
|
||||
type: object
|
||||
type: object
|
||||
siteUrl:
|
||||
description: |-
|
||||
SiteURL is referring to the URL of the (frontend) application
|
||||
In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress
|
||||
type: string
|
||||
workloadTemplate:
|
||||
properties:
|
||||
additionalLabels:
|
||||
|
@ -883,9 +873,6 @@ spec:
|
|||
required:
|
||||
- securityContext
|
||||
type: object
|
||||
required:
|
||||
- externalUrl
|
||||
- siteUrl
|
||||
type: object
|
||||
database:
|
||||
properties:
|
||||
|
@ -1010,6 +997,11 @@ spec:
|
|||
type: boolean
|
||||
type: object
|
||||
type: object
|
||||
externalUrl:
|
||||
description: |-
|
||||
APIExternalURL is referring to the URL where Supabase API will be available
|
||||
Typically this is the ingress of the API gateway
|
||||
type: string
|
||||
jwt:
|
||||
properties:
|
||||
anonKey:
|
||||
|
@ -1775,6 +1767,14 @@ spec:
|
|||
- securityContext
|
||||
type: object
|
||||
type: object
|
||||
siteUrl:
|
||||
description: |-
|
||||
SiteURL is referring to the URL of the (frontend) application
|
||||
In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress
|
||||
type: string
|
||||
required:
|
||||
- externalUrl
|
||||
- siteUrl
|
||||
type: object
|
||||
status:
|
||||
description: CoreStatus defines the observed state of Core.
|
||||
|
|
|
@ -71,7 +71,6 @@ spec:
|
|||
- host
|
||||
type: object
|
||||
pgMeta:
|
||||
default: {}
|
||||
description: PGMeta
|
||||
properties:
|
||||
workloadTemplate:
|
||||
|
@ -764,9 +763,57 @@ spec:
|
|||
type: object
|
||||
type: object
|
||||
studio:
|
||||
default: {}
|
||||
description: Studio
|
||||
properties:
|
||||
externalUrl:
|
||||
description: |-
|
||||
APIExternalURL is referring to the URL where Supabase API will be available
|
||||
Typically this is the ingress of the API gateway
|
||||
type: string
|
||||
gatewayServiceSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
default:
|
||||
app.kubernetes.io/component: api-gateway
|
||||
app.kubernetes.io/name: envoy
|
||||
description: |-
|
||||
GatewayServiceSelector - selector to find the service for the API gateway
|
||||
Required to configure the API URL in the studio deployment
|
||||
If you don't run multiple APIGateway instances in the same namespaces, the default will be fine
|
||||
type: object
|
||||
jwt:
|
||||
properties:
|
||||
anonKey:
|
||||
default: anon_key
|
||||
description: AnonKey - key in secret where to read the anon
|
||||
JWT from
|
||||
type: string
|
||||
secretKey:
|
||||
default: secret
|
||||
description: SecretKey - key in secret where to read the JWT
|
||||
HMAC secret from
|
||||
type: string
|
||||
secretRef:
|
||||
description: SecretRef - object reference to the Secret where
|
||||
JWT values are stored
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
serviceKey:
|
||||
default: service_key
|
||||
description: ServiceKey - key in secret where to read the
|
||||
service JWT from
|
||||
type: string
|
||||
type: object
|
||||
workloadTemplate:
|
||||
description: WorkloadTemplate - customize the studio deployment
|
||||
properties:
|
||||
|
@ -1455,6 +1502,8 @@ spec:
|
|||
required:
|
||||
- securityContext
|
||||
type: object
|
||||
required:
|
||||
- externalUrl
|
||||
type: object
|
||||
required:
|
||||
- db
|
||||
|
|
|
@ -13,13 +13,19 @@ metadata:
|
|||
app.kubernetes.io/managed-by: kustomize
|
||||
name: core-sample
|
||||
spec:
|
||||
externalUrl: http://localhost:8000/
|
||||
siteUrl: http://localhost:3000/
|
||||
database:
|
||||
dsnFrom:
|
||||
name: supabase-demo-credentials
|
||||
key: url
|
||||
auth:
|
||||
externalUrl: http://localhost:8000/
|
||||
siteUrl: http://localhost:3000/
|
||||
disableSignup: true
|
||||
enableEmailAutoconfirm: true
|
||||
providers: {}
|
||||
postgrest:
|
||||
maxRows: 1000
|
||||
jwt:
|
||||
expiry: 3600
|
||||
secretRef:
|
||||
name: core-sample-jwt
|
||||
|
|
|
@ -7,7 +7,15 @@ metadata:
|
|||
name: core-sample
|
||||
spec:
|
||||
db:
|
||||
host: cluster-example-rw.supabase-demo
|
||||
host: cluster-example-rw.supabase-demo.svc
|
||||
dbName: app
|
||||
dbCredentialsRef:
|
||||
name: db-roles-creds-supabase-admin
|
||||
studio:
|
||||
externalUrl: http://localhost:8000
|
||||
jwt:
|
||||
anonKey: anon_key
|
||||
secretKey: secret
|
||||
secretRef:
|
||||
name: core-sample-jwt
|
||||
serviceKey: service_key
|
||||
|
|
|
@ -44,6 +44,26 @@ webhooks:
|
|||
resources:
|
||||
- cores
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
path: /mutate-supabase-k8s-icb4dc0-de-v1alpha1-dashboard
|
||||
failurePolicy: Fail
|
||||
name: mdashboard-v1alpha1.kb.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
apiVersions:
|
||||
- v1alpha1
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- dashboards
|
||||
sideEffects: None
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
|
@ -90,3 +110,23 @@ webhooks:
|
|||
resources:
|
||||
- cores
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
path: /validate-supabase-k8s-icb4dc0-de-v1alpha1-dashboard
|
||||
failurePolicy: Fail
|
||||
name: vdashboard-v1alpha1.kb.io
|
||||
rules:
|
||||
- apiGroups:
|
||||
- supabase.k8s.icb4dc0.de
|
||||
apiVersions:
|
||||
- v1alpha1
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- dashboards
|
||||
sideEffects: None
|
||||
|
|
|
@ -125,8 +125,6 @@ _Appears in:_
|
|||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
|
||||
| `siteUrl` _string_ | SiteURL is referring to the URL of the (frontend) application<br />In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress | | |
|
||||
| `additionalRedirectUrls` _string array_ | | | |
|
||||
| `disableSignup` _boolean_ | | | |
|
||||
| `anonymousUsersEnabled` _boolean_ | | | |
|
||||
|
@ -212,7 +210,9 @@ _Appears in:_
|
|||
| `spec` _[CoreSpec](#corespec)_ | | | |
|
||||
|
||||
|
||||
#### CoreCondition
|
||||
|
||||
|
||||
#### CoreJwtSpec
|
||||
|
||||
|
||||
|
||||
|
@ -221,28 +221,17 @@ _Appears in:_
|
|||
|
||||
|
||||
_Appears in:_
|
||||
- [CoreStatus](#corestatus)
|
||||
- [CoreSpec](#corespec)
|
||||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `type` _[CoreConditionType](#coreconditiontype)_ | | | |
|
||||
| `lastProbeTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
||||
| `lastTransitionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
||||
| `reason` _string_ | | | |
|
||||
| `message` _string_ | | | |
|
||||
|
||||
|
||||
#### CoreConditionType
|
||||
|
||||
_Underlying type:_ _string_
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_Appears in:_
|
||||
- [CoreCondition](#corecondition)
|
||||
|
||||
| `secret` _string_ | Secret - JWT HMAC secret in plain text<br />This is WRITE-ONLY and will be copied to the SecretRef by the defaulter | | |
|
||||
| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | SecretRef - object reference to the Secret where JWT values are stored | | |
|
||||
| `secretKey` _string_ | SecretKey - key in secret where to read the JWT HMAC secret from | secret | |
|
||||
| `jwksKey` _string_ | JwksKey - key in secret where to read the JWKS from | jwks.json | |
|
||||
| `anonKey` _string_ | AnonKey - key in secret where to read the anon JWT from | anon_key | |
|
||||
| `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | |
|
||||
| `expiry` _integer_ | Expiry - expiration time in seconds for JWTs | 3600 | |
|
||||
|
||||
|
||||
#### CoreList
|
||||
|
@ -276,7 +265,9 @@ _Appears in:_
|
|||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `jwt` _[JwtSpec](#jwtspec)_ | | | |
|
||||
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
|
||||
| `siteUrl` _string_ | SiteURL is referring to the URL of the (frontend) application<br />In most Kubernetes scenarios this is the same as the APIExternalURL with a different path handler in the ingress | | |
|
||||
| `jwt` _[CoreJwtSpec](#corejwtspec)_ | | | |
|
||||
| `database` _[Database](#database)_ | | | |
|
||||
| `postgrest` _[PostgrestSpec](#postgrestspec)_ | | | |
|
||||
| `auth` _[AuthSpec](#authspec)_ | | | |
|
||||
|
@ -322,6 +313,25 @@ _Appears in:_
|
|||
| `dbCredentialsRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from<br />Credentials need to be stored in basic auth form | | |
|
||||
|
||||
|
||||
#### DashboardJwtSpec
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_Appears in:_
|
||||
- [StudioSpec](#studiospec)
|
||||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | SecretRef - object reference to the Secret where JWT values are stored | | |
|
||||
| `secretKey` _string_ | SecretKey - key in secret where to read the JWT HMAC secret from | secret | |
|
||||
| `anonKey` _string_ | AnonKey - key in secret where to read the anon JWT from | anon_key | |
|
||||
| `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | |
|
||||
|
||||
|
||||
#### DashboardList
|
||||
|
||||
|
||||
|
@ -354,8 +364,8 @@ _Appears in:_
|
|||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `db` _[DashboardDbSpec](#dashboarddbspec)_ | | | |
|
||||
| `pgMeta` _[PGMetaSpec](#pgmetaspec)_ | PGMeta | \{ \} | |
|
||||
| `studio` _[StudioSpec](#studiospec)_ | Studio | \{ \} | |
|
||||
| `pgMeta` _[PGMetaSpec](#pgmetaspec)_ | PGMeta | | |
|
||||
| `studio` _[StudioSpec](#studiospec)_ | Studio | | |
|
||||
|
||||
|
||||
|
||||
|
@ -526,31 +536,9 @@ _Appears in:_
|
|||
| `pullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | | |
|
||||
|
||||
|
||||
#### JwtSpec
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_Appears in:_
|
||||
- [CoreSpec](#corespec)
|
||||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `secret` _string_ | Secret - JWT HMAC secret in plain text<br />This is WRITE-ONLY and will be copied to the SecretRef by the defaulter | | |
|
||||
| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | SecretRef - object reference to the Secret where JWT values are stored | | |
|
||||
| `secretKey` _string_ | SecretKey - key in secret where to read the JWT HMAC secret from | secret | |
|
||||
| `jwksKey` _string_ | JwksKey - key in secret where to read the JWKS from | jwks.json | |
|
||||
| `anonKey` _string_ | AnonKey - key in secret where to read the anon JWT from | anon_key | |
|
||||
| `serviceKey` _string_ | ServiceKey - key in secret where to read the service JWT from | service_key | |
|
||||
| `expiry` _integer_ | Expiry - expiration time in seconds for JWTs | 3600 | |
|
||||
|
||||
|
||||
#### MigrationStatus
|
||||
|
||||
_Underlying type:_ _object_
|
||||
_Underlying type:_ _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_
|
||||
|
||||
|
||||
|
||||
|
@ -645,7 +633,10 @@ _Appears in:_
|
|||
|
||||
| Field | Description | Default | Validation |
|
||||
| --- | --- | --- | --- |
|
||||
| `jwt` _[DashboardJwtSpec](#dashboardjwtspec)_ | | | |
|
||||
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the studio deployment | | |
|
||||
| `gatewayServiceSelector` _object (keys:string, values:string)_ | GatewayServiceSelector - selector to find the service for the API gateway<br />Required to configure the API URL in the studio deployment<br />If you don't run multiple APIGateway instances in the same namespaces, the default will be fine | \{ app.kubernetes.io/component:api-gateway app.kubernetes.io/name:envoy \} | |
|
||||
| `externalUrl` _string_ | APIExternalURL is referring to the URL where Supabase API will be available<br />Typically this is the ingress of the API gateway | | |
|
||||
|
||||
|
||||
#### WorkloadTemplate
|
||||
|
|
2
go.mod
2
go.mod
|
@ -13,6 +13,7 @@ require (
|
|||
github.com/onsi/ginkgo/v2 v2.19.0
|
||||
github.com/onsi/gomega v1.33.1
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
@ -94,7 +95,6 @@ require (
|
|||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
|
|
|
@ -178,7 +178,7 @@ func (r *APIGatewayReconciler) reconcileEnvoyConfig(
|
|||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, configMap, func() error {
|
||||
configMap.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag), gateway.Labels)
|
||||
configMap.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag), gateway.Labels)
|
||||
|
||||
type nodeSpec struct {
|
||||
Cluster string
|
||||
|
@ -245,58 +245,15 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
|||
},
|
||||
}
|
||||
|
||||
envoySpec := gateway.Spec.Envoy
|
||||
|
||||
if envoySpec == nil {
|
||||
envoySpec = new(supabasev1alpha1.EnvoySpec)
|
||||
}
|
||||
|
||||
if envoySpec.WorkloadTemplate == nil {
|
||||
envoySpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||
}
|
||||
|
||||
if envoySpec.WorkloadTemplate.Workload == nil {
|
||||
envoySpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||
}
|
||||
|
||||
var (
|
||||
image = supabase.Images.Envoy.String()
|
||||
podSecurityContext = envoySpec.WorkloadTemplate.SecurityContext
|
||||
pullPolicy = envoySpec.WorkloadTemplate.Workload.PullPolicy
|
||||
containerSecurityContext = envoySpec.WorkloadTemplate.Workload.SecurityContext
|
||||
envoySpec = gateway.Spec.Envoy
|
||||
serviceCfg = supabase.ServiceConfig.Envoy
|
||||
)
|
||||
|
||||
if img := envoySpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||
image = img
|
||||
}
|
||||
|
||||
if podSecurityContext == nil {
|
||||
podSecurityContext = &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
}
|
||||
}
|
||||
|
||||
if containerSecurityContext == nil {
|
||||
containerSecurityContext = &corev1.SecurityContext{
|
||||
Privileged: ptrOf(false),
|
||||
RunAsUser: ptrOf(int64(65532)),
|
||||
RunAsGroup: ptrOf(int64(65532)),
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
AllowPrivilegeEscalation: ptrOf(false),
|
||||
ReadOnlyRootFilesystem: ptrOf(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyDeployment, func() error {
|
||||
envoyDeployment.Labels = MergeLabels(
|
||||
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag),
|
||||
envoyDeployment.Labels = envoySpec.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||
gateway.Labels,
|
||||
envoySpec.WorkloadTemplate.AdditionalLabels,
|
||||
)
|
||||
|
||||
if envoyDeployment.CreationTimestamp.IsZero() {
|
||||
|
@ -305,7 +262,7 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
|||
}
|
||||
}
|
||||
|
||||
envoyDeployment.Spec.Replicas = envoySpec.WorkloadTemplate.Replicas
|
||||
envoyDeployment.Spec.Replicas = envoySpec.WorkloadTemplate.ReplicaCount()
|
||||
|
||||
envoyDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -313,19 +270,16 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
|||
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "config-hash"): configHash,
|
||||
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwks-hash"): jwksHash,
|
||||
},
|
||||
Labels: MergeLabels(
|
||||
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||
envoySpec.WorkloadTemplate.AdditionalLabels,
|
||||
),
|
||||
Labels: objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ImagePullSecrets: envoySpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||
ImagePullSecrets: envoySpec.WorkloadTemplate.PullSecrets(),
|
||||
AutomountServiceAccountToken: ptrOf(false),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "envoy-proxy",
|
||||
Image: image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Image: envoySpec.WorkloadTemplate.Image(supabase.Images.Envoy.String()),
|
||||
ImagePullPolicy: envoySpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Args: []string{"-c /etc/envoy/config.yaml"},
|
||||
Ports: []corev1.ContainerPort{
|
||||
{
|
||||
|
@ -362,18 +316,18 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
|||
},
|
||||
},
|
||||
},
|
||||
SecurityContext: containerSecurityContext,
|
||||
Resources: envoySpec.WorkloadTemplate.Workload.Resources,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
SecurityContext: envoySpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
||||
Resources: envoySpec.WorkloadTemplate.Resources(),
|
||||
VolumeMounts: envoySpec.WorkloadTemplate.AdditionalVolumeMounts(
|
||||
corev1.VolumeMount{
|
||||
Name: "config",
|
||||
ReadOnly: true,
|
||||
MountPath: "/etc/envoy",
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
SecurityContext: podSecurityContext,
|
||||
SecurityContext: envoySpec.WorkloadTemplate.PodSecurityContext(),
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
|
@ -432,10 +386,10 @@ func (r *APIGatewayReconciler) reconcileEnvoyService(
|
|||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyService, func() error {
|
||||
envoyService.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag), gateway.Labels)
|
||||
envoyService.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag), gateway.Labels)
|
||||
|
||||
envoyService.Spec = corev1.ServiceSpec{
|
||||
Selector: selectorLabels(gateway, "postgrest"),
|
||||
Selector: selectorLabels(gateway, "envoy"),
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "rest",
|
||||
|
|
|
@ -74,50 +74,9 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
}
|
||||
authSpec = core.Spec.Auth
|
||||
svcCfg = supabase.ServiceConfig.Auth
|
||||
)
|
||||
|
||||
if authSpec.WorkloadTemplate == nil {
|
||||
authSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||
}
|
||||
|
||||
if authSpec.WorkloadTemplate.Workload == nil {
|
||||
authSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||
}
|
||||
|
||||
var (
|
||||
image = supabase.Images.Gotrue.String()
|
||||
podSecurityContext = authSpec.WorkloadTemplate.SecurityContext
|
||||
pullPolicy = authSpec.WorkloadTemplate.Workload.PullPolicy
|
||||
containerSecurityContext = authSpec.WorkloadTemplate.Workload.SecurityContext
|
||||
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||
)
|
||||
|
||||
if img := authSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||
image = img
|
||||
}
|
||||
|
||||
if podSecurityContext == nil {
|
||||
podSecurityContext = &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
}
|
||||
}
|
||||
|
||||
if containerSecurityContext == nil {
|
||||
containerSecurityContext = &corev1.SecurityContext{
|
||||
Privileged: ptrOf(false),
|
||||
RunAsUser: ptrOf(int64(1000)),
|
||||
RunAsGroup: ptrOf(int64(1000)),
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
AllowPrivilegeEscalation: ptrOf(false),
|
||||
ReadOnlyRootFilesystem: ptrOf(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -129,7 +88,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, authDeployment, func() error {
|
||||
authDeployment.Labels = MergeLabels(
|
||||
authDeployment.Labels = authSpec.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||
core.Labels,
|
||||
)
|
||||
|
@ -153,24 +112,24 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
}
|
||||
|
||||
authEnv := append(authDbEnv,
|
||||
svcCfg.EnvKeys.ApiHost.Var(svcCfg.Defaults.ApiHost),
|
||||
svcCfg.EnvKeys.ApiPort.Var(svcCfg.Defaults.ApiPort),
|
||||
svcCfg.EnvKeys.ApiExternalUrl.Var(authSpec.APIExternalURL),
|
||||
svcCfg.EnvKeys.DBDriver.Var(svcCfg.Defaults.DbDriver),
|
||||
svcCfg.EnvKeys.SiteUrl.Var(authSpec.SiteURL),
|
||||
svcCfg.EnvKeys.ApiHost.Var(),
|
||||
svcCfg.EnvKeys.ApiPort.Var(),
|
||||
svcCfg.EnvKeys.ApiExternalUrl.Var(core.Spec.APIExternalURL),
|
||||
svcCfg.EnvKeys.DBDriver.Var(),
|
||||
svcCfg.EnvKeys.SiteUrl.Var(core.Spec.SiteURL),
|
||||
svcCfg.EnvKeys.AdditionalRedirectURLs.Var(authSpec.AdditionalRedirectUrls),
|
||||
svcCfg.EnvKeys.DisableSignup.Var(boolValueOf(authSpec.DisableSignup)),
|
||||
svcCfg.EnvKeys.JWTIssuer.Var(svcCfg.Defaults.JwtIssuer),
|
||||
svcCfg.EnvKeys.JWTAdminRoles.Var(svcCfg.Defaults.JwtAdminRoles),
|
||||
svcCfg.EnvKeys.JWTAudience.Var(svcCfg.Defaults.JwtAudience),
|
||||
svcCfg.EnvKeys.JwtDefaultGroup.Var(svcCfg.Defaults.JwtDefaultGroupName),
|
||||
svcCfg.EnvKeys.JWTIssuer.Var(),
|
||||
svcCfg.EnvKeys.JWTAdminRoles.Var(),
|
||||
svcCfg.EnvKeys.JWTAudience.Var(),
|
||||
svcCfg.EnvKeys.JwtDefaultGroup.Var(),
|
||||
svcCfg.EnvKeys.JwtExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
||||
svcCfg.EnvKeys.JwtSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||
svcCfg.EnvKeys.EmailSignupDisabled.Var(boolValueOf(authSpec.EmailSignupDisabled)),
|
||||
svcCfg.EnvKeys.AnonymousUsersEnabled.Var(boolValueOf(authSpec.AnonymousUsersEnabled)),
|
||||
)
|
||||
|
||||
authEnv = append(authEnv, authSpec.Providers.Vars(authSpec.APIExternalURL)...)
|
||||
authEnv = append(authEnv, authSpec.Providers.Vars(core.Spec.APIExternalURL)...)
|
||||
|
||||
if authDeployment.CreationTimestamp.IsZero() {
|
||||
authDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||
|
@ -178,38 +137,38 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
}
|
||||
}
|
||||
|
||||
authDeployment.Spec.Replicas = authSpec.WorkloadTemplate.Replicas
|
||||
authDeployment.Spec.Replicas = authSpec.WorkloadTemplate.ReplicaCount()
|
||||
|
||||
authDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ImagePullSecrets: authSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||
ImagePullSecrets: authSpec.WorkloadTemplate.PullSecrets(),
|
||||
InitContainers: []corev1.Container{{
|
||||
Name: "migrations",
|
||||
Image: image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Name: "supabase-auth-migrations",
|
||||
Image: authSpec.WorkloadTemplate.Image(supabase.Images.Gotrue.String()),
|
||||
ImagePullPolicy: authSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Command: []string{"/usr/local/bin/auth"},
|
||||
Args: []string{"migrate"},
|
||||
Env: authEnv,
|
||||
SecurityContext: containerSecurityContext,
|
||||
Env: authSpec.WorkloadTemplate.MergeEnv(authEnv),
|
||||
SecurityContext: authSpec.WorkloadTemplate.ContainerSecurityContext(svcCfg.Defaults.UID, svcCfg.Defaults.GID),
|
||||
}},
|
||||
Containers: []corev1.Container{{
|
||||
Name: "supabase-auth",
|
||||
Image: image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Image: authSpec.WorkloadTemplate.Image(supabase.Images.Gotrue.String()),
|
||||
ImagePullPolicy: authSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Command: []string{"/usr/local/bin/auth"},
|
||||
Args: []string{"serve"},
|
||||
Env: MergeEnv(authEnv, authSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||
Env: authSpec.WorkloadTemplate.MergeEnv(authEnv),
|
||||
Ports: []corev1.ContainerPort{{
|
||||
Name: "api",
|
||||
ContainerPort: 9999,
|
||||
ContainerPort: svcCfg.Defaults.APIPort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
SecurityContext: containerSecurityContext,
|
||||
Resources: authSpec.WorkloadTemplate.Workload.Resources,
|
||||
VolumeMounts: authSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||
SecurityContext: authSpec.WorkloadTemplate.ContainerSecurityContext(svcCfg.Defaults.UID, svcCfg.Defaults.GID),
|
||||
Resources: authSpec.WorkloadTemplate.Resources(),
|
||||
VolumeMounts: authSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 5,
|
||||
PeriodSeconds: 3,
|
||||
|
@ -218,7 +177,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/health",
|
||||
Port: intstr.IntOrString{IntVal: 9999},
|
||||
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -229,12 +188,12 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/health",
|
||||
Port: intstr.IntOrString{IntVal: 9999},
|
||||
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
SecurityContext: podSecurityContext,
|
||||
SecurityContext: authSpec.WorkloadTemplate.PodSecurityContext(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -257,12 +216,14 @@ func (r *CoreAuthReconciler) reconcileAuthService(
|
|||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, authService, func() error {
|
||||
authService.Labels = MergeLabels(
|
||||
authService.Labels = core.Spec.Postgrest.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||
core.Labels,
|
||||
)
|
||||
|
||||
if _, ok := authService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||
authService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||
}
|
||||
|
||||
authService.Spec = corev1.ServiceSpec{
|
||||
Selector: selectorLabels(core, "auth"),
|
||||
|
|
|
@ -78,51 +78,13 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
postgrestSpec = core.Spec.Postgrest
|
||||
)
|
||||
|
||||
if postgrestSpec.WorkloadTemplate == nil {
|
||||
postgrestSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||
}
|
||||
|
||||
if postgrestSpec.WorkloadTemplate.Workload == nil {
|
||||
postgrestSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||
}
|
||||
|
||||
var (
|
||||
image = supabase.Images.Postgrest.String()
|
||||
podSecurityContext = postgrestSpec.WorkloadTemplate.SecurityContext
|
||||
pullPolicy = postgrestSpec.WorkloadTemplate.Workload.PullPolicy
|
||||
containerSecurityContext = postgrestSpec.WorkloadTemplate.Workload.SecurityContext
|
||||
anonRole = ValueOrFallback(postgrestSpec.AnonRole, serviceCfg.Defaults.AnonRole)
|
||||
postgrestSchemas = ValueOrFallback(postgrestSpec.Schemas, serviceCfg.Defaults.Schemas)
|
||||
jwtSecretHash string
|
||||
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||
)
|
||||
|
||||
if img := postgrestSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||
image = img
|
||||
}
|
||||
|
||||
if podSecurityContext == nil {
|
||||
podSecurityContext = &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
}
|
||||
}
|
||||
|
||||
if containerSecurityContext == nil {
|
||||
containerSecurityContext = &corev1.SecurityContext{
|
||||
Privileged: ptrOf(false),
|
||||
RunAsUser: ptrOf(int64(1000)),
|
||||
RunAsGroup: ptrOf(int64(1000)),
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
AllowPrivilegeEscalation: ptrOf(false),
|
||||
ReadOnlyRootFilesystem: ptrOf(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -140,7 +102,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, postgrestDeployment, func() error {
|
||||
postgrestDeployment.Labels = MergeLabels(
|
||||
postgrestDeployment.Labels = postgrestSpec.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||
core.Labels,
|
||||
)
|
||||
|
@ -161,6 +123,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
Name: serviceCfg.EnvKeys.DBUri,
|
||||
Value: strings.TrimSuffix(fmt.Sprintf("postgres://%s:$(DB_CREDENTIALS_PASSWORD)@%s%s?%s", supabase.DBRoleAuthenticator, parsedDSN.Host, parsedDSN.Path, parsedDSN.Query().Encode()), "?"),
|
||||
},
|
||||
serviceCfg.EnvKeys.Host.Var(),
|
||||
serviceCfg.EnvKeys.JWTSecret.Var(core.Spec.JWT.JwksKeySelector()),
|
||||
serviceCfg.EnvKeys.Schemas.Var(postgrestSchemas),
|
||||
serviceCfg.EnvKeys.AnonRole.Var(anonRole),
|
||||
|
@ -168,7 +131,9 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
serviceCfg.EnvKeys.ExtraSearchPath.Var(serviceCfg.Defaults.ExtraSearchPath),
|
||||
serviceCfg.EnvKeys.AppSettingsJWTSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||
serviceCfg.EnvKeys.AppSettingsJWTExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
||||
serviceCfg.EnvKeys.AdminServerPort.Var(3001),
|
||||
serviceCfg.EnvKeys.AdminServerPort.Var((serviceCfg.Defaults.AdminPort)),
|
||||
serviceCfg.EnvKeys.MaxRows.Var(postgrestSpec.MaxRows),
|
||||
serviceCfg.EnvKeys.OpenAPIProxyURI.Var(fmt.Sprintf("%s/rest/v1", strings.TrimSuffix(core.Spec.APIExternalURL, "/"))),
|
||||
}
|
||||
|
||||
if postgrestDeployment.CreationTimestamp.IsZero() {
|
||||
|
@ -177,7 +142,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
}
|
||||
}
|
||||
|
||||
postgrestDeployment.Spec.Replicas = postgrestSpec.WorkloadTemplate.Replicas
|
||||
postgrestDeployment.Spec.Replicas = postgrestSpec.WorkloadTemplate.ReplicaCount()
|
||||
|
||||
postgrestDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -187,29 +152,29 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
Labels: objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ImagePullSecrets: postgrestSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||
ImagePullSecrets: postgrestSpec.WorkloadTemplate.PullSecrets(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "supabase-rest",
|
||||
Image: image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Image: postgrestSpec.WorkloadTemplate.Image(supabase.Images.Postgrest.String()),
|
||||
ImagePullPolicy: postgrestSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Args: []string{"postgrest"},
|
||||
Env: MergeEnv(postgrestEnv, postgrestSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||
Env: postgrestSpec.WorkloadTemplate.MergeEnv(postgrestEnv),
|
||||
Ports: []corev1.ContainerPort{
|
||||
{
|
||||
Name: "rest",
|
||||
ContainerPort: 3000,
|
||||
ContainerPort: serviceCfg.Defaults.ServerPort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
},
|
||||
{
|
||||
Name: "admin",
|
||||
ContainerPort: 3001,
|
||||
ContainerPort: serviceCfg.Defaults.AdminPort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
},
|
||||
},
|
||||
SecurityContext: containerSecurityContext,
|
||||
Resources: postgrestSpec.WorkloadTemplate.Workload.Resources,
|
||||
VolumeMounts: postgrestSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||
SecurityContext: postgrestSpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
||||
Resources: postgrestSpec.WorkloadTemplate.Resources(),
|
||||
VolumeMounts: postgrestSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 5,
|
||||
PeriodSeconds: 3,
|
||||
|
@ -218,7 +183,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/ready",
|
||||
Port: intstr.IntOrString{IntVal: 3001},
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.AdminPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -229,13 +194,13 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/live",
|
||||
Port: intstr.IntOrString{IntVal: 3001},
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.AdminPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SecurityContext: podSecurityContext,
|
||||
SecurityContext: postgrestSpec.WorkloadTemplate.PodSecurityContext(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -258,12 +223,14 @@ func (r *CorePostgrestReconiler) reconcilePostgrestService(
|
|||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, postgrestService, func() error {
|
||||
postgrestService.Labels = MergeLabels(
|
||||
postgrestService.Labels = core.Spec.Postgrest.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(core, supabase.ServiceConfig.Postgrest.Name, "core", supabase.Images.Postgrest.Tag),
|
||||
core.Labels,
|
||||
)
|
||||
|
||||
if _, ok := postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||
postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||
}
|
||||
|
||||
postgrestService.Spec = corev1.ServiceSpec{
|
||||
Selector: selectorLabels(core, supabase.ServiceConfig.Postgrest.Name),
|
||||
|
|
|
@ -40,19 +40,6 @@ type DashboardPGMetaReconciler struct {
|
|||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the Dashboard object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||
func (r *DashboardPGMetaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
var (
|
||||
dashboard supabasev1alpha1.Dashboard
|
||||
|
@ -101,47 +88,6 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
pgMetaSpec = new(supabasev1alpha1.PGMetaSpec)
|
||||
}
|
||||
|
||||
if pgMetaSpec.WorkloadTemplate == nil {
|
||||
pgMetaSpec.WorkloadTemplate = new(supabasev1alpha1.WorkloadTemplate)
|
||||
}
|
||||
|
||||
if pgMetaSpec.WorkloadTemplate.Workload == nil {
|
||||
pgMetaSpec.WorkloadTemplate.Workload = new(supabasev1alpha1.ContainerTemplate)
|
||||
}
|
||||
|
||||
var (
|
||||
image = supabase.Images.PostgresMeta.String()
|
||||
podSecurityContext = pgMetaSpec.WorkloadTemplate.SecurityContext
|
||||
pullPolicy = pgMetaSpec.WorkloadTemplate.Workload.PullPolicy
|
||||
containerSecurityContext = pgMetaSpec.WorkloadTemplate.Workload.SecurityContext
|
||||
)
|
||||
|
||||
if img := pgMetaSpec.WorkloadTemplate.Workload.Image; img != "" {
|
||||
image = img
|
||||
}
|
||||
|
||||
if podSecurityContext == nil {
|
||||
podSecurityContext = &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
}
|
||||
}
|
||||
|
||||
if containerSecurityContext == nil {
|
||||
containerSecurityContext = &corev1.SecurityContext{
|
||||
Privileged: ptrOf(false),
|
||||
RunAsUser: ptrOf(int64(1000)),
|
||||
RunAsGroup: ptrOf(int64(1000)),
|
||||
RunAsNonRoot: ptrOf(true),
|
||||
AllowPrivilegeEscalation: ptrOf(false),
|
||||
ReadOnlyRootFilesystem: ptrOf(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
dsnSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: dashboard.Spec.DBSpec.DBCredentialsRef.Name,
|
||||
|
@ -153,7 +99,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, pgMetaDeployment, func() error {
|
||||
pgMetaDeployment.Labels = MergeLabels(
|
||||
pgMetaDeployment.Labels = pgMetaSpec.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||
dashboard.Labels,
|
||||
)
|
||||
|
@ -164,11 +110,12 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
}
|
||||
}
|
||||
|
||||
pgMetaDeployment.Spec.Replicas = pgMetaSpec.WorkloadTemplate.Replicas
|
||||
pgMetaDeployment.Spec.Replicas = pgMetaSpec.WorkloadTemplate.ReplicaCount()
|
||||
|
||||
pgMetaEnv := []corev1.EnvVar{
|
||||
serviceCfg.EnvKeys.APIPort.Var(serviceCfg.Defaults.APIPort),
|
||||
serviceCfg.EnvKeys.DBHost.Var(dashboard.Spec.DBSpec.Host),
|
||||
serviceCfg.EnvKeys.DBName.Var(dashboard.Spec.DBSpec.DBName),
|
||||
serviceCfg.EnvKeys.DBPort.Var(dashboard.Spec.DBSpec.Port),
|
||||
serviceCfg.EnvKeys.DBUser.Var(dashboard.Spec.DBSpec.UserRef()),
|
||||
serviceCfg.EnvKeys.DBPassword.Var(dashboard.Spec.DBSpec.PasswordRef()),
|
||||
|
@ -179,20 +126,20 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
Labels: objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ImagePullSecrets: pgMetaSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
||||
ImagePullSecrets: pgMetaSpec.WorkloadTemplate.PullSecrets(),
|
||||
Containers: []corev1.Container{{
|
||||
Name: "supabase-meta",
|
||||
Image: image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Env: MergeEnv(pgMetaEnv, pgMetaSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
||||
Image: pgMetaSpec.WorkloadTemplate.Image(supabase.Images.PostgresMeta.String()),
|
||||
ImagePullPolicy: pgMetaSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Env: pgMetaSpec.WorkloadTemplate.MergeEnv(pgMetaEnv),
|
||||
Ports: []corev1.ContainerPort{{
|
||||
Name: "api",
|
||||
ContainerPort: int32(serviceCfg.Defaults.APIPort),
|
||||
ContainerPort: serviceCfg.Defaults.APIPort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
SecurityContext: containerSecurityContext,
|
||||
Resources: pgMetaSpec.WorkloadTemplate.Workload.Resources,
|
||||
VolumeMounts: pgMetaSpec.WorkloadTemplate.Workload.VolumeMounts,
|
||||
SecurityContext: pgMetaSpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.NodeUID, serviceCfg.Defaults.NodeGID),
|
||||
Resources: pgMetaSpec.WorkloadTemplate.Resources(),
|
||||
VolumeMounts: pgMetaSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 5,
|
||||
PeriodSeconds: 3,
|
||||
|
@ -201,7 +148,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/health",
|
||||
Port: intstr.IntOrString{IntVal: int32(serviceCfg.Defaults.APIPort)},
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -212,12 +159,12 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
|||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/health",
|
||||
Port: intstr.IntOrString{IntVal: int32(serviceCfg.Defaults.APIPort)},
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
SecurityContext: podSecurityContext,
|
||||
SecurityContext: pgMetaSpec.WorkloadTemplate.PodSecurityContext(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -239,15 +186,19 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaService(
|
|||
ObjectMeta: supabase.ServiceConfig.PGMeta.ObjectMeta(dashboard),
|
||||
}
|
||||
|
||||
if dashboard.Spec.PGMeta == nil {
|
||||
dashboard.Spec.PGMeta = new(supabasev1alpha1.PGMetaSpec)
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrPatch(ctx, r.Client, pgMetaService, func() error {
|
||||
pgMetaService.Labels = MergeLabels(
|
||||
pgMetaService.Labels = dashboard.Spec.PGMeta.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(dashboard, supabase.ServiceConfig.PGMeta.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||
dashboard.Labels,
|
||||
)
|
||||
|
||||
if _, ok := pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||
pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
|
||||
|
||||
apiPort := int32(supabase.ServiceConfig.PGMeta.Defaults.APIPort)
|
||||
}
|
||||
|
||||
pgMetaService.Spec = corev1.ServiceSpec{
|
||||
Selector: selectorLabels(dashboard, supabase.ServiceConfig.PGMeta.Name),
|
||||
|
@ -256,8 +207,8 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaService(
|
|||
Name: "api",
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
AppProtocol: ptrOf("http"),
|
||||
Port: apiPort,
|
||||
TargetPort: intstr.IntOrString{IntVal: apiPort},
|
||||
Port: supabase.ServiceConfig.PGMeta.Defaults.APIPort,
|
||||
TargetPort: intstr.IntOrString{IntVal: supabase.ServiceConfig.PGMeta.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
246
internal/controller/dashboard_studio_controller.go
Normal file
246
internal/controller/dashboard_studio_controller.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
Copyright 2024 Peter Kurfer.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||
)
|
||||
|
||||
type DashboardStudioReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *DashboardStudioReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
var (
|
||||
dashboard supabasev1alpha1.Dashboard
|
||||
logger = log.FromContext(ctx)
|
||||
)
|
||||
|
||||
if err := r.Get(ctx, req.NamespacedName, &dashboard); client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "unable to fetch Dashboard")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.reconcileStudioDeployment(ctx, &dashboard); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.reconcileStudioService(ctx, &dashboard); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *DashboardStudioReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&supabasev1alpha1.Dashboard{}).
|
||||
Owns(new(appsv1.Deployment)).
|
||||
Owns(new(corev1.Service)).
|
||||
Named("dashboard-studio").
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *DashboardStudioReconciler) reconcileStudioDeployment(
|
||||
ctx context.Context,
|
||||
dashboard *supabasev1alpha1.Dashboard,
|
||||
) error {
|
||||
var (
|
||||
serviceCfg = supabase.ServiceConfig.Studio
|
||||
studioDeployment = &appsv1.Deployment{
|
||||
ObjectMeta: serviceCfg.ObjectMeta(dashboard),
|
||||
}
|
||||
studioSpec = dashboard.Spec.Studio
|
||||
gatewayServiceList corev1.ServiceList
|
||||
)
|
||||
|
||||
if studioSpec == nil {
|
||||
studioSpec = new(supabasev1alpha1.StudioSpec)
|
||||
}
|
||||
|
||||
err := r.List(
|
||||
ctx,
|
||||
&gatewayServiceList,
|
||||
client.InNamespace(dashboard.Namespace),
|
||||
client.MatchingLabels(studioSpec.GatewayServiceMatchLabels),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("selecting gateway service: %w", err)
|
||||
}
|
||||
|
||||
if itemCount := len(gatewayServiceList.Items); itemCount != 1 {
|
||||
return fmt.Errorf("unexpected matches for gateway service: %d", itemCount)
|
||||
}
|
||||
|
||||
gatewayService := gatewayServiceList.Items[0]
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, studioDeployment, func() error {
|
||||
studioDeployment.Labels = studioSpec.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.Studio.Tag),
|
||||
dashboard.Labels,
|
||||
)
|
||||
|
||||
if studioDeployment.CreationTimestamp.IsZero() {
|
||||
studioDeployment.Spec.Selector = &metav1.LabelSelector{
|
||||
MatchLabels: selectorLabels(dashboard, serviceCfg.Name),
|
||||
}
|
||||
}
|
||||
|
||||
studioDeployment.Spec.Replicas = studioSpec.WorkloadTemplate.ReplicaCount()
|
||||
|
||||
studioEnv := []corev1.EnvVar{
|
||||
serviceCfg.EnvKeys.PGMetaURL.Var(fmt.Sprintf("http://%s.%s.svc:%d", supabase.ServiceConfig.PGMeta.ObjectName(dashboard), dashboard.Namespace, supabase.ServiceConfig.PGMeta.Defaults.APIPort)),
|
||||
serviceCfg.EnvKeys.Host.Var(),
|
||||
serviceCfg.EnvKeys.ApiUrl.Var(fmt.Sprintf("http://%s.%s.svc:8000", gatewayService.Name, gatewayService.Namespace)),
|
||||
serviceCfg.EnvKeys.DBPassword.Var(dashboard.Spec.DBSpec.PasswordRef()),
|
||||
serviceCfg.EnvKeys.APIExternalURL.Var(studioSpec.APIExternalURL),
|
||||
serviceCfg.EnvKeys.JwtSecret.Var(studioSpec.JWT.SecretKeySelector()),
|
||||
serviceCfg.EnvKeys.AnonKey.Var(studioSpec.JWT.AnonKeySelector()),
|
||||
serviceCfg.EnvKeys.ServiceKey.Var(studioSpec.JWT.ServiceKeySelector()),
|
||||
}
|
||||
|
||||
studioDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.Studio.Tag),
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ImagePullSecrets: studioSpec.WorkloadTemplate.PullSecrets(),
|
||||
Containers: []corev1.Container{{
|
||||
Name: "supabase-studio",
|
||||
Image: studioSpec.WorkloadTemplate.Image(supabase.Images.Studio.String()),
|
||||
ImagePullPolicy: studioSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||
Env: studioSpec.WorkloadTemplate.MergeEnv(studioEnv),
|
||||
Ports: []corev1.ContainerPort{{
|
||||
Name: "studio",
|
||||
ContainerPort: serviceCfg.Defaults.APIPort,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
SecurityContext: studioSpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.NodeUID, serviceCfg.Defaults.NodeGID),
|
||||
Resources: studioSpec.WorkloadTemplate.Resources(),
|
||||
VolumeMounts: studioSpec.WorkloadTemplate.AdditionalVolumeMounts(corev1.VolumeMount{
|
||||
Name: "next-cache",
|
||||
MountPath: "/app/apps/studio/.next/cache",
|
||||
}),
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 5,
|
||||
PeriodSeconds: 3,
|
||||
TimeoutSeconds: 1,
|
||||
SuccessThreshold: 2,
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/api/profile",
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
LivenessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 10,
|
||||
PeriodSeconds: 5,
|
||||
TimeoutSeconds: 3,
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/api/profile",
|
||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
SecurityContext: studioSpec.WorkloadTemplate.PodSecurityContext(),
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "next-cache",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{
|
||||
Medium: corev1.StorageMediumDefault,
|
||||
SizeLimit: ptrOf(resource.MustParse("300Mi")),
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
if err := controllerutil.SetControllerReference(dashboard, studioDeployment, r.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DashboardStudioReconciler) reconcileStudioService(
|
||||
ctx context.Context,
|
||||
dashboard *supabasev1alpha1.Dashboard,
|
||||
) error {
|
||||
studioService := &corev1.Service{
|
||||
ObjectMeta: supabase.ServiceConfig.Studio.ObjectMeta(dashboard),
|
||||
}
|
||||
|
||||
if dashboard.Spec.Studio == nil {
|
||||
dashboard.Spec.Studio = new(supabasev1alpha1.StudioSpec)
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrPatch(ctx, r.Client, studioService, func() error {
|
||||
studioService.Labels = dashboard.Spec.Studio.WorkloadTemplate.MergeLabels(
|
||||
objectLabels(dashboard, supabase.ServiceConfig.Studio.Name, "dashboard", supabase.Images.Studio.Tag),
|
||||
dashboard.Labels,
|
||||
)
|
||||
|
||||
if _, ok := studioService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||
studioService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
|
||||
}
|
||||
|
||||
studioService.Spec = corev1.ServiceSpec{
|
||||
Selector: selectorLabels(dashboard, supabase.ServiceConfig.Studio.Name),
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "studio",
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
AppProtocol: ptrOf("http"),
|
||||
Port: supabase.ServiceConfig.Studio.Defaults.APIPort,
|
||||
TargetPort: intstr.IntOrString{IntVal: supabase.ServiceConfig.Studio.Defaults.APIPort},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := controllerutil.SetControllerReference(dashboard, studioService, r.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
|
@ -6,6 +6,9 @@ package controller
|
|||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=apigateways/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=dashboards/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=secrets;configmaps;services,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=events,verbs=create
|
||||
|
|
|
@ -26,7 +26,7 @@ import (
|
|||
"time"
|
||||
|
||||
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
|
||||
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
|
@ -176,6 +176,7 @@ type envoyClusterServices struct {
|
|||
Postgrest *PostgrestCluster
|
||||
GoTrue *GoTrueCluster
|
||||
PGMeta *PGMetaCluster
|
||||
Studio *StudioCluster
|
||||
}
|
||||
|
||||
func (s *envoyClusterServices) UpsertEndpoints(eps *discoveryv1.EndpointSlice) {
|
||||
|
@ -200,12 +201,13 @@ func (s *envoyClusterServices) UpsertEndpoints(eps *discoveryv1.EndpointSlice) {
|
|||
|
||||
func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapshot, error) {
|
||||
const (
|
||||
routeName = "supabase"
|
||||
apiRouteName = "supabase"
|
||||
studioRouteName = "supabas-studio"
|
||||
vHostName = "supabase"
|
||||
listenerName = "supabase"
|
||||
)
|
||||
|
||||
manager := &hcm.HttpConnectionManager{
|
||||
apiConnectionManager := &hcm.HttpConnectionManager{
|
||||
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||
StatPrefix: "http",
|
||||
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||
|
@ -225,7 +227,7 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
|||
},
|
||||
},
|
||||
},
|
||||
RouteConfigName: routeName,
|
||||
RouteConfigName: apiRouteName,
|
||||
},
|
||||
},
|
||||
HttpFilters: []*hcm.HttpFilter{
|
||||
|
@ -244,8 +246,39 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
|||
},
|
||||
}
|
||||
|
||||
routeCfg := &route.RouteConfiguration{
|
||||
Name: routeName,
|
||||
studioConnetionManager := &hcm.HttpConnectionManager{
|
||||
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||
StatPrefix: "http",
|
||||
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||
Rds: &hcm.Rds{
|
||||
ConfigSource: &corev3.ConfigSource{
|
||||
ResourceApiVersion: resource.DefaultAPIVersion,
|
||||
ConfigSourceSpecifier: &corev3.ConfigSource_ApiConfigSource{
|
||||
ApiConfigSource: &corev3.ApiConfigSource{
|
||||
TransportApiVersion: resource.DefaultAPIVersion,
|
||||
ApiType: corev3.ApiConfigSource_GRPC,
|
||||
SetNodeOnFirstMessageOnly: true,
|
||||
GrpcServices: []*corev3.GrpcService{{
|
||||
TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{
|
||||
EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ClusterName: "supabase-control-plane"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
RouteConfigName: studioRouteName,
|
||||
},
|
||||
},
|
||||
HttpFilters: []*hcm.HttpFilter{
|
||||
{
|
||||
Name: FilterNameHttpRouter,
|
||||
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
apiRouteCfg := &route.RouteConfiguration{
|
||||
Name: apiRouteName,
|
||||
VirtualHosts: []*route.VirtualHost{{
|
||||
Name: "supabase",
|
||||
Domains: []string{"*"},
|
||||
|
@ -264,7 +297,9 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
|||
},
|
||||
}
|
||||
|
||||
listener := &listener.Listener{
|
||||
// TODO add studio route config
|
||||
|
||||
listeners := []*listenerv3.Listener{{
|
||||
Name: listenerName,
|
||||
Address: &corev3.Address{
|
||||
Address: &corev3.Address_SocketAddress{
|
||||
|
@ -277,18 +312,47 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
|||
},
|
||||
},
|
||||
},
|
||||
FilterChains: []*listener.FilterChain{
|
||||
FilterChains: []*listenerv3.FilterChain{
|
||||
{
|
||||
Filters: []*listener.Filter{
|
||||
Filters: []*listenerv3.Filter{
|
||||
{
|
||||
Name: FilterNameHttpConnectionManager,
|
||||
ConfigType: &listener.Filter_TypedConfig{
|
||||
TypedConfig: MustAny(manager),
|
||||
ConfigType: &listenerv3.Filter_TypedConfig{
|
||||
TypedConfig: MustAny(apiConnectionManager),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
if s.Studio != nil {
|
||||
listeners = append(listeners, &listenerv3.Listener{
|
||||
Name: "studio",
|
||||
Address: &corev3.Address{
|
||||
Address: &corev3.Address_SocketAddress{
|
||||
SocketAddress: &corev3.SocketAddress{
|
||||
Protocol: corev3.SocketAddress_TCP,
|
||||
Address: "0.0.0.0",
|
||||
PortSpecifier: &corev3.SocketAddress_PortValue{
|
||||
PortValue: 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FilterChains: []*listenerv3.FilterChain{
|
||||
{
|
||||
Filters: []*listenerv3.Filter{
|
||||
{
|
||||
Name: FilterNameHttpConnectionManager,
|
||||
ConfigType: &listenerv3.Filter_TypedConfig{
|
||||
TypedConfig: MustAny(studioConnetionManager),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
rawSnapshot := map[resource.Type][]types.Resource{
|
||||
|
@ -298,8 +362,8 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
|||
s.GoTrue.Cluster(instance),
|
||||
s.PGMeta.Cluster(instance),
|
||||
)...),
|
||||
resource.RouteType: {routeCfg},
|
||||
resource.ListenerType: {listener},
|
||||
resource.RouteType: {apiRouteCfg},
|
||||
resource.ListenerType: castResources(listeners...),
|
||||
}
|
||||
|
||||
snapshot, err := cache.NewSnapshot(
|
||||
|
|
|
@ -33,7 +33,7 @@ func (c *PostgrestCluster) Routes(instance string) []*routev3.Route {
|
|||
Name: "PostgREST: /rest/v1/* -> http://rest:3000/*",
|
||||
Match: &routev3.RouteMatch{
|
||||
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||
Prefix: "/rest/v1",
|
||||
Prefix: "/rest/v1/",
|
||||
},
|
||||
},
|
||||
Action: &routev3.Route_Route{
|
||||
|
|
5
internal/controlplane/studio.go
Normal file
5
internal/controlplane/studio.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package controlplane
|
||||
|
||||
type StudioCluster struct {
|
||||
ServiceCluster
|
||||
}
|
|
@ -2,63 +2,10 @@ package supabase
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type stringEnv string
|
||||
|
||||
func (e stringEnv) Var(value string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type stringSliceEnv struct {
|
||||
key string
|
||||
separator string
|
||||
}
|
||||
|
||||
func (e stringSliceEnv) Var(value []string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: e.key,
|
||||
Value: strings.Join(value, e.separator),
|
||||
}
|
||||
}
|
||||
|
||||
type intEnv string
|
||||
|
||||
func (e intEnv) Var(value int) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: strconv.Itoa(value),
|
||||
}
|
||||
}
|
||||
|
||||
type boolEnv string
|
||||
|
||||
func (e boolEnv) Var(value bool) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: strconv.FormatBool(value),
|
||||
}
|
||||
}
|
||||
|
||||
type secretEnv string
|
||||
|
||||
func (e secretEnv) Var(sel *corev1.SecretKeySelector) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: sel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type serviceConfig[TEnvKeys, TDefaults any] struct {
|
||||
Name string
|
||||
EnvKeys TEnvKeys
|
||||
|
@ -74,6 +21,7 @@ func (cfg serviceConfig[TEnvKeys, TDefaults]) ObjectMeta(obj metav1.Object) meta
|
|||
}
|
||||
|
||||
type postgrestEnvKeys struct {
|
||||
Host fixedEnv
|
||||
DBUri string
|
||||
Schemas stringSliceEnv
|
||||
AnonRole stringEnv
|
||||
|
@ -81,31 +29,34 @@ type postgrestEnvKeys struct {
|
|||
UseLegacyGucs boolEnv
|
||||
ExtraSearchPath stringSliceEnv
|
||||
AppSettingsJWTSecret secretEnv
|
||||
AppSettingsJWTExpiry intEnv
|
||||
AdminServerPort intEnv
|
||||
MaxRows intEnv
|
||||
AppSettingsJWTExpiry intEnv[int]
|
||||
AdminServerPort intEnv[int32]
|
||||
MaxRows intEnv[int]
|
||||
OpenAPIProxyURI stringEnv
|
||||
}
|
||||
|
||||
type postgrestConfigDefaults struct {
|
||||
AnonRole string
|
||||
Schemas []string
|
||||
ExtraSearchPath []string
|
||||
UID, GID int64
|
||||
ServerPort, AdminPort int32
|
||||
}
|
||||
|
||||
type authEnvKeys struct {
|
||||
ApiHost stringEnv
|
||||
ApiPort intEnv
|
||||
ApiHost fixedEnv
|
||||
ApiPort fixedEnv
|
||||
ApiExternalUrl stringEnv
|
||||
DBDriver stringEnv
|
||||
DBDriver fixedEnv
|
||||
DatabaseUrl string
|
||||
SiteUrl stringEnv
|
||||
AdditionalRedirectURLs stringSliceEnv
|
||||
DisableSignup boolEnv
|
||||
JWTIssuer stringEnv
|
||||
JWTAdminRoles stringEnv
|
||||
JWTAudience stringEnv
|
||||
JwtDefaultGroup stringEnv
|
||||
JwtExpiry intEnv
|
||||
JWTIssuer fixedEnv
|
||||
JWTAdminRoles fixedEnv
|
||||
JWTAudience fixedEnv
|
||||
JwtDefaultGroup fixedEnv
|
||||
JwtExpiry intEnv[int]
|
||||
JwtSecret secretEnv
|
||||
EmailSignupDisabled boolEnv
|
||||
MailerUrlPathsInvite stringEnv
|
||||
|
@ -116,35 +67,50 @@ type authEnvKeys struct {
|
|||
}
|
||||
|
||||
type authConfigDefaults struct {
|
||||
ApiHost string
|
||||
ApiPort int
|
||||
DbDriver string
|
||||
JwtIssuer string
|
||||
JwtAdminRoles string
|
||||
JwtAudience string
|
||||
JwtDefaultGroupName string
|
||||
MailerUrlPathsInvite string
|
||||
MailerUrlPathsConfirmation string
|
||||
MailerUrlPathsRecovery string
|
||||
MailerUrlPathsEmailChange string
|
||||
APIPort int32
|
||||
UID, GID int64
|
||||
}
|
||||
|
||||
type pgMetaEnvKeys struct {
|
||||
APIPort intEnv
|
||||
APIPort intEnv[int32]
|
||||
DBHost stringEnv
|
||||
DBPort intEnv
|
||||
DBPort intEnv[int]
|
||||
DBName stringEnv
|
||||
DBUser secretEnv
|
||||
DBPassword secretEnv
|
||||
}
|
||||
|
||||
type pgMetaDefaults struct {
|
||||
APIPort int
|
||||
APIPort int32
|
||||
DBPort string
|
||||
NodeUID int64
|
||||
NodeGID int64
|
||||
}
|
||||
|
||||
type studioEnvKeys struct {
|
||||
PGMetaURL stringEnv
|
||||
DBPassword secretEnv
|
||||
ApiUrl stringEnv
|
||||
APIExternalURL stringEnv
|
||||
JwtSecret secretEnv
|
||||
AnonKey secretEnv
|
||||
ServiceKey secretEnv
|
||||
Host fixedEnv
|
||||
}
|
||||
|
||||
type studioDefaults struct {
|
||||
NodeUID int64
|
||||
NodeGID int64
|
||||
APIPort int32
|
||||
}
|
||||
|
||||
type envoyDefaults struct {
|
||||
ConfigKey string
|
||||
UID, GID int64
|
||||
}
|
||||
|
||||
type envoyServiceConfig struct {
|
||||
|
@ -176,12 +142,14 @@ var ServiceConfig = struct {
|
|||
Postgrest serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]
|
||||
Auth serviceConfig[authEnvKeys, authConfigDefaults]
|
||||
PGMeta serviceConfig[pgMetaEnvKeys, pgMetaDefaults]
|
||||
Studio serviceConfig[studioEnvKeys, studioDefaults]
|
||||
Envoy envoyServiceConfig
|
||||
JWT jwtConfig
|
||||
}{
|
||||
Postgrest: serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]{
|
||||
Name: "postgrest",
|
||||
EnvKeys: postgrestEnvKeys{
|
||||
Host: fixedEnvOf("PGRST_SERVER_HOST", "*"),
|
||||
DBUri: "PGRST_DB_URI",
|
||||
Schemas: stringSliceEnv{key: "PGRST_DB_SCHEMAS", separator: ","},
|
||||
AnonRole: "PGRST_DB_ANON_ROLE",
|
||||
|
@ -191,28 +159,34 @@ var ServiceConfig = struct {
|
|||
AppSettingsJWTExpiry: "PGRST_APP_SETTINGS_JWT_EXP",
|
||||
AdminServerPort: "PGRST_ADMIN_SERVER_PORT",
|
||||
ExtraSearchPath: stringSliceEnv{key: "PGRST_DB_EXTRA_SEARCH_PATH", separator: ","},
|
||||
MaxRows: "PGRST_DB_MAX_ROWS",
|
||||
OpenAPIProxyURI: "PGRST_OPENAPI_SERVER_PROXY_URI",
|
||||
},
|
||||
Defaults: postgrestConfigDefaults{
|
||||
AnonRole: "anon",
|
||||
Schemas: []string{"public", "graphql_public"},
|
||||
ExtraSearchPath: []string{"public", "extensions"},
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
ServerPort: 3000,
|
||||
AdminPort: 3001,
|
||||
},
|
||||
},
|
||||
Auth: serviceConfig[authEnvKeys, authConfigDefaults]{
|
||||
Name: "auth",
|
||||
EnvKeys: authEnvKeys{
|
||||
ApiHost: "GOTRUE_API_HOST",
|
||||
ApiPort: "GOTRUE_API_PORT",
|
||||
ApiHost: fixedEnvOf("GOTRUE_API_HOST", "0.0.0.0"),
|
||||
ApiPort: fixedEnvOf("GOTRUE_API_PORT", "9999"),
|
||||
ApiExternalUrl: "API_EXTERNAL_URL",
|
||||
DBDriver: "GOTRUE_DB_DRIVER",
|
||||
DBDriver: fixedEnvOf("GOTRUE_DB_DRIVER", "postgres"),
|
||||
DatabaseUrl: "GOTRUE_DB_DATABASE_URL",
|
||||
SiteUrl: "GOTRUE_SITE_URL",
|
||||
AdditionalRedirectURLs: stringSliceEnv{key: "GOTRUE_URI_ALLOW_LIST", separator: ","},
|
||||
DisableSignup: "GOTRUE_DISABLE_SIGNUP",
|
||||
JWTIssuer: "GOTRUE_JWT_ISSUER",
|
||||
JWTAdminRoles: "GOTRUE_JWT_ADMIN_ROLES",
|
||||
JWTAudience: "GOTRUE_JWT_AUD",
|
||||
JwtDefaultGroup: "GOTRUE_JWT_DEFAULT_GROUP_NAME",
|
||||
JWTIssuer: fixedEnvOf("GOTRUE_JWT_ISSUER", "supabase"),
|
||||
JWTAdminRoles: fixedEnvOf("GOTRUE_JWT_ADMIN_ROLES", "service_role"),
|
||||
JWTAudience: fixedEnvOf("GOTRUE_JWT_AUD", "authenticated"),
|
||||
JwtDefaultGroup: fixedEnvOf("GOTRUE_JWT_DEFAULT_GROUP_NAME", "authenticated"),
|
||||
JwtExpiry: "GOTRUE_JWT_EXP",
|
||||
JwtSecret: "GOTRUE_JWT_SECRET",
|
||||
EmailSignupDisabled: "GOTRUE_EXTERNAL_EMAIL_ENABLED",
|
||||
|
@ -223,17 +197,13 @@ var ServiceConfig = struct {
|
|||
AnonymousUsersEnabled: "GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED",
|
||||
},
|
||||
Defaults: authConfigDefaults{
|
||||
ApiHost: "0.0.0.0",
|
||||
ApiPort: 9999,
|
||||
DbDriver: "postgres",
|
||||
JwtIssuer: "supabase",
|
||||
JwtAdminRoles: "service_role",
|
||||
JwtAudience: "authenticated",
|
||||
JwtDefaultGroupName: "authenticated",
|
||||
MailerUrlPathsInvite: "/auth/v1/verify",
|
||||
MailerUrlPathsConfirmation: "/auth/v1/verify",
|
||||
MailerUrlPathsRecovery: "/auth/v1/verify",
|
||||
MailerUrlPathsEmailChange: "/auth/v1/verify",
|
||||
APIPort: 9999,
|
||||
UID: 1000,
|
||||
GID: 1000,
|
||||
},
|
||||
},
|
||||
PGMeta: serviceConfig[pgMetaEnvKeys, pgMetaDefaults]{
|
||||
|
@ -249,11 +219,33 @@ var ServiceConfig = struct {
|
|||
Defaults: pgMetaDefaults{
|
||||
APIPort: 8080,
|
||||
DBPort: "5432",
|
||||
NodeUID: 1000,
|
||||
NodeGID: 1000,
|
||||
},
|
||||
},
|
||||
Studio: serviceConfig[studioEnvKeys, studioDefaults]{
|
||||
Name: "studio",
|
||||
EnvKeys: studioEnvKeys{
|
||||
PGMetaURL: "STUDIO_PG_META_URL",
|
||||
DBPassword: "POSTGRES_PASSWORD",
|
||||
ApiUrl: "SUPABASE_URL",
|
||||
APIExternalURL: "SUPABASE_PUBLIC_URL",
|
||||
JwtSecret: "AUTH_JWT_SECRET",
|
||||
AnonKey: "SUPABASE_ANON_KEY",
|
||||
ServiceKey: "SUPABASE_SERVICE_KEY",
|
||||
Host: fixedEnvOf("HOSTNAME", "0.0.0.0"),
|
||||
},
|
||||
Defaults: studioDefaults{
|
||||
NodeUID: 1000,
|
||||
NodeGID: 1000,
|
||||
APIPort: 3000,
|
||||
},
|
||||
},
|
||||
Envoy: envoyServiceConfig{
|
||||
Defaults: envoyDefaults{
|
||||
"config.yaml",
|
||||
ConfigKey: "config.yaml",
|
||||
UID: 65532,
|
||||
GID: 65532,
|
||||
},
|
||||
},
|
||||
JWT: jwtConfig{
|
||||
|
|
78
internal/supabase/env_types.go
Normal file
78
internal/supabase/env_types.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package supabase
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func fixedEnvOf(key, value string) fixedEnv {
|
||||
return fixedEnvFunc(func() corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: key,
|
||||
Value: value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type fixedEnvFunc func() corev1.EnvVar
|
||||
|
||||
func (f fixedEnvFunc) Var() corev1.EnvVar {
|
||||
return f()
|
||||
}
|
||||
|
||||
type fixedEnv interface {
|
||||
Var() corev1.EnvVar
|
||||
}
|
||||
|
||||
type stringEnv string
|
||||
|
||||
func (e stringEnv) Var(value string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type stringSliceEnv struct {
|
||||
key string
|
||||
separator string
|
||||
}
|
||||
|
||||
func (e stringSliceEnv) Var(value []string) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: e.key,
|
||||
Value: strings.Join(value, e.separator),
|
||||
}
|
||||
}
|
||||
|
||||
type intEnv[T constraints.Integer] string
|
||||
|
||||
func (e intEnv[T]) Var(value T) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: strconv.FormatInt(int64(value), 10),
|
||||
}
|
||||
}
|
||||
|
||||
type boolEnv string
|
||||
|
||||
func (e boolEnv) Var(value bool) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
Value: strconv.FormatBool(value),
|
||||
}
|
||||
}
|
||||
|
||||
type secretEnv string
|
||||
|
||||
func (e secretEnv) Var(sel *corev1.SecretKeySelector) corev1.EnvVar {
|
||||
return corev1.EnvVar{
|
||||
Name: string(e),
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: sel,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -111,7 +111,7 @@ func (d *CoreCustomDefaulter) defaultJWT(ctx context.Context, core *supabasev1al
|
|||
corelog.Info("Defaulting JWT")
|
||||
|
||||
if core.Spec.JWT == nil {
|
||||
core.Spec.JWT = new(supabasev1alpha1.JwtSpec)
|
||||
core.Spec.JWT = new(supabasev1alpha1.CoreJwtSpec)
|
||||
}
|
||||
|
||||
if core.Spec.JWT.SecretRef == nil {
|
||||
|
|
114
internal/webhook/v1alpha1/dashboard_webhook.go
Normal file
114
internal/webhook/v1alpha1/dashboard_webhook.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
Copyright 2024 Peter Kurfer.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
// nolint:unused
|
||||
// log is for logging in this package.
|
||||
var dashboardlog = logf.Log.WithName("dashboard-resource")
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-supabase-k8s-icb4dc0-de-v1alpha1-dashboard,mutating=true,failurePolicy=fail,sideEffects=None,groups=supabase.k8s.icb4dc0.de,resources=dashboards,verbs=create;update,versions=v1alpha1,name=mdashboard-v1alpha1.kb.io,admissionReviewVersions=v1
|
||||
|
||||
// DashboardCustomDefaulter struct is responsible for setting default values on the custom resource of the
|
||||
// Kind Dashboard when those are created or updated.
|
||||
//
|
||||
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
|
||||
// as it is used only for temporary operations and does not need to be deeply copied.
|
||||
type DashboardCustomDefaulter struct {
|
||||
// TODO(user): Add more fields as needed for defaulting
|
||||
}
|
||||
|
||||
var _ webhook.CustomDefaulter = &DashboardCustomDefaulter{}
|
||||
|
||||
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Dashboard.
|
||||
func (d *DashboardCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
|
||||
dashboard, ok := obj.(*supabasev1alpha1.Dashboard)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("expected an Dashboard object but got %T", obj)
|
||||
}
|
||||
dashboardlog.Info("Defaulting for Dashboard", "name", dashboard.GetName())
|
||||
|
||||
// TODO(user): fill in your defaulting logic.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
|
||||
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
|
||||
// +kubebuilder:webhook:path=/validate-supabase-k8s-icb4dc0-de-v1alpha1-dashboard,mutating=false,failurePolicy=fail,sideEffects=None,groups=supabase.k8s.icb4dc0.de,resources=dashboards,verbs=create;update,versions=v1alpha1,name=vdashboard-v1alpha1.kb.io,admissionReviewVersions=v1
|
||||
|
||||
// DashboardCustomValidator struct is responsible for validating the Dashboard resource
|
||||
// when it is created, updated, or deleted.
|
||||
//
|
||||
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
|
||||
// as this struct is used only for temporary operations and does not need to be deeply copied.
|
||||
type DashboardCustomValidator struct {
|
||||
// TODO(user): Add more fields as needed for validation
|
||||
}
|
||||
|
||||
var _ webhook.CustomValidator = &DashboardCustomValidator{}
|
||||
|
||||
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Dashboard.
|
||||
func (v *DashboardCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
||||
dashboard, ok := obj.(*supabasev1alpha1.Dashboard)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected a Dashboard object but got %T", obj)
|
||||
}
|
||||
dashboardlog.Info("Validation for Dashboard upon creation", "name", dashboard.GetName())
|
||||
|
||||
// TODO(user): fill in your validation logic upon object creation.
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Dashboard.
|
||||
func (v *DashboardCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
|
||||
dashboard, ok := newObj.(*supabasev1alpha1.Dashboard)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected a Dashboard object for the newObj but got %T", newObj)
|
||||
}
|
||||
dashboardlog.Info("Validation for Dashboard upon update", "name", dashboard.GetName())
|
||||
|
||||
// TODO(user): fill in your validation logic upon object update.
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Dashboard.
|
||||
func (v *DashboardCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
|
||||
dashboard, ok := obj.(*supabasev1alpha1.Dashboard)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected a Dashboard object but got %T", obj)
|
||||
}
|
||||
dashboardlog.Info("Validation for Dashboard upon deletion", "name", dashboard.GetName())
|
||||
|
||||
// TODO(user): fill in your validation logic upon object deletion.
|
||||
|
||||
return nil, nil
|
||||
}
|
86
internal/webhook/v1alpha1/dashboard_webhook_test.go
Normal file
86
internal/webhook/v1alpha1/dashboard_webhook_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2024 Peter Kurfer.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||
// TODO (user): Add any additional imports if needed
|
||||
)
|
||||
|
||||
var _ = Describe("Dashboard Webhook", func() {
|
||||
var (
|
||||
obj *supabasev1alpha1.Dashboard
|
||||
oldObj *supabasev1alpha1.Dashboard
|
||||
validator DashboardCustomValidator
|
||||
defaulter DashboardCustomDefaulter
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
obj = &supabasev1alpha1.Dashboard{}
|
||||
oldObj = &supabasev1alpha1.Dashboard{}
|
||||
validator = DashboardCustomValidator{}
|
||||
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
|
||||
defaulter = DashboardCustomDefaulter{}
|
||||
Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
|
||||
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
|
||||
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
|
||||
// TODO (user): Add any setup logic common to all tests
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO (user): Add any teardown logic common to all tests
|
||||
})
|
||||
|
||||
Context("When creating Dashboard under Defaulting Webhook", func() {
|
||||
// TODO (user): Add logic for defaulting webhooks
|
||||
// Example:
|
||||
// It("Should apply defaults when a required field is empty", func() {
|
||||
// By("simulating a scenario where defaults should be applied")
|
||||
// obj.SomeFieldWithDefault = ""
|
||||
// By("calling the Default method to apply defaults")
|
||||
// defaulter.Default(ctx, obj)
|
||||
// By("checking that the default values are set")
|
||||
// Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
|
||||
// })
|
||||
})
|
||||
|
||||
Context("When creating or updating Dashboard under Validating Webhook", func() {
|
||||
// TODO (user): Add logic for validating webhooks
|
||||
// Example:
|
||||
// It("Should deny creation if a required field is missing", func() {
|
||||
// By("simulating an invalid creation scenario")
|
||||
// obj.SomeRequiredField = ""
|
||||
// Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
|
||||
// })
|
||||
//
|
||||
// It("Should admit creation if all required fields are present", func() {
|
||||
// By("simulating an invalid creation scenario")
|
||||
// obj.SomeRequiredField = "valid_value"
|
||||
// Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
|
||||
// })
|
||||
//
|
||||
// It("Should validate updates correctly", func() {
|
||||
// By("simulating a valid update scenario")
|
||||
// oldObj.SomeRequiredField = "updated_value"
|
||||
// obj.SomeRequiredField = "updated_value"
|
||||
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
|
||||
// })
|
||||
})
|
||||
})
|
|
@ -29,3 +29,11 @@ func SetupCoreWebhookWithManager(mgr ctrl.Manager) error {
|
|||
WithDefaulter(&CoreCustomDefaulter{Client: mgr.GetClient()}).
|
||||
Complete()
|
||||
}
|
||||
|
||||
// SetupDashboardWebhookWithManager registers the webhook for Dashboard in the manager.
|
||||
func SetupDashboardWebhookWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewWebhookManagedBy(mgr).For(&supabasev1alpha1.Dashboard{}).
|
||||
WithValidator(&DashboardCustomValidator{}).
|
||||
WithDefaulter(&DashboardCustomDefaulter{}).
|
||||
Complete()
|
||||
}
|
||||
|
|
|
@ -124,6 +124,9 @@ var _ = BeforeSuite(func() {
|
|||
err = SetupAPIGatewayWebhookWithManager(mgr, WebhookConfig{CurrentNamespace: "default"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = SetupDashboardWebhookWithManager(mgr)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:webhook
|
||||
|
||||
go func() {
|
||||
|
|
901
openapi.json
Normal file
901
openapi.json
Normal file
|
@ -0,0 +1,901 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "",
|
||||
"title": "standard public schema",
|
||||
"version": "12.2.0 (ec89f6b)"
|
||||
},
|
||||
"host": "localhost:8000",
|
||||
"basePath": "/rest/v1",
|
||||
"schemes": ["http"],
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"application/vnd.pgrst.object+json;nulls=stripped",
|
||||
"application/vnd.pgrst.object+json",
|
||||
"text/csv"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/vnd.pgrst.object+json;nulls=stripped",
|
||||
"application/vnd.pgrst.object+json",
|
||||
"text/csv"
|
||||
],
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"produces": ["application/openapi+json", "application/json"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"summary": "OpenAPI description (this document)",
|
||||
"tags": ["Introspection"]
|
||||
}
|
||||
},
|
||||
"/lists": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.user_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/order"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/range"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rangeUnit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/offset"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/limit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferCount"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/lists"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"description": "Partial Content"
|
||||
}
|
||||
},
|
||||
"tags": ["lists"]
|
||||
},
|
||||
"post": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/body.lists"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferPost"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
}
|
||||
},
|
||||
"tags": ["lists"]
|
||||
},
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.user_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["lists"]
|
||||
},
|
||||
"patch": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.user_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.lists.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/body.lists"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["lists"]
|
||||
}
|
||||
},
|
||||
"/tasks": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.list_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.category_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.description"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.due_date"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.priority"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.completed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/order"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/range"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rangeUnit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/offset"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/limit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferCount"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/tasks"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"description": "Partial Content"
|
||||
}
|
||||
},
|
||||
"tags": ["tasks"]
|
||||
},
|
||||
"post": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/body.tasks"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferPost"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
}
|
||||
},
|
||||
"tags": ["tasks"]
|
||||
},
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.list_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.category_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.description"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.due_date"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.priority"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.completed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["tasks"]
|
||||
},
|
||||
"patch": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.list_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.category_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.description"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.due_date"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.priority"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.tasks.completed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/body.tasks"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["tasks"]
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.username"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.email"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.password_hash"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/order"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/range"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rangeUnit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/offset"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/limit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferCount"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/users"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"description": "Partial Content"
|
||||
}
|
||||
},
|
||||
"tags": ["users"]
|
||||
},
|
||||
"post": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/body.users"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferPost"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
}
|
||||
},
|
||||
"tags": ["users"]
|
||||
},
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.username"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.email"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.password_hash"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["users"]
|
||||
},
|
||||
"patch": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.username"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.email"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.users.password_hash"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/body.users"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["users"]
|
||||
}
|
||||
},
|
||||
"/categories": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/order"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/range"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rangeUnit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/offset"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/limit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferCount"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/categories"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"description": "Partial Content"
|
||||
}
|
||||
},
|
||||
"tags": ["categories"]
|
||||
},
|
||||
"post": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/body.categories"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferPost"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created"
|
||||
}
|
||||
},
|
||||
"tags": ["categories"]
|
||||
},
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["categories"]
|
||||
},
|
||||
"patch": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/rowFilter.categories.name"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/body.categories"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/preferReturn"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"tags": ["categories"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"lists": {
|
||||
"required": ["id", "user_id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Note:\nThis is a Primary Key.<pk/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"description": "Note:\nThis is a Foreign Key to `users.id`.<fk table='users' column='id'/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"tasks": {
|
||||
"required": ["id", "list_id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Note:\nThis is a Primary Key.<pk/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"list_id": {
|
||||
"description": "Note:\nThis is a Foreign Key to `lists.id`.<fk table='lists' column='id'/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"category_id": {
|
||||
"description": "Note:\nThis is a Foreign Key to `categories.id`.<fk table='categories' column='id'/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
},
|
||||
"due_date": {
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"format": "integer",
|
||||
"type": "integer"
|
||||
},
|
||||
"completed": {
|
||||
"default": false,
|
||||
"format": "boolean",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"users": {
|
||||
"required": ["id", "username", "email", "password_hash"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Note:\nThis is a Primary Key.<pk/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"username": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
},
|
||||
"password_hash": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"categories": {
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Note:\nThis is a Primary Key.<pk/>",
|
||||
"format": "bigint",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"format": "text",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"preferParams": {
|
||||
"name": "Prefer",
|
||||
"description": "Preference",
|
||||
"required": false,
|
||||
"enum": ["params=single-object"],
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"preferReturn": {
|
||||
"name": "Prefer",
|
||||
"description": "Preference",
|
||||
"required": false,
|
||||
"enum": ["return=representation", "return=minimal", "return=none"],
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"preferCount": {
|
||||
"name": "Prefer",
|
||||
"description": "Preference",
|
||||
"required": false,
|
||||
"enum": ["count=none"],
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"preferPost": {
|
||||
"name": "Prefer",
|
||||
"description": "Preference",
|
||||
"required": false,
|
||||
"enum": [
|
||||
"return=representation",
|
||||
"return=minimal",
|
||||
"return=none",
|
||||
"resolution=ignore-duplicates",
|
||||
"resolution=merge-duplicates"
|
||||
],
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"select": {
|
||||
"name": "select",
|
||||
"description": "Filtering Columns",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"on_conflict": {
|
||||
"name": "on_conflict",
|
||||
"description": "On Conflict",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"description": "Ordering",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"name": "Range",
|
||||
"description": "Limiting and Pagination",
|
||||
"required": false,
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"rangeUnit": {
|
||||
"name": "Range-Unit",
|
||||
"description": "Limiting and Pagination",
|
||||
"required": false,
|
||||
"default": "items",
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
"offset": {
|
||||
"name": "offset",
|
||||
"description": "Limiting and Pagination",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"limit": {
|
||||
"name": "limit",
|
||||
"description": "Limiting and Pagination",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"body.lists": {
|
||||
"name": "lists",
|
||||
"description": "lists",
|
||||
"required": false,
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lists"
|
||||
}
|
||||
},
|
||||
"rowFilter.lists.id": {
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.lists.user_id": {
|
||||
"name": "user_id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.lists.name": {
|
||||
"name": "name",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"body.tasks": {
|
||||
"name": "tasks",
|
||||
"description": "tasks",
|
||||
"required": false,
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/tasks"
|
||||
}
|
||||
},
|
||||
"rowFilter.tasks.id": {
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.list_id": {
|
||||
"name": "list_id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.category_id": {
|
||||
"name": "category_id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.name": {
|
||||
"name": "name",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.description": {
|
||||
"name": "description",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.due_date": {
|
||||
"name": "due_date",
|
||||
"required": false,
|
||||
"format": "date",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.priority": {
|
||||
"name": "priority",
|
||||
"required": false,
|
||||
"format": "integer",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.tasks.completed": {
|
||||
"name": "completed",
|
||||
"required": false,
|
||||
"format": "boolean",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"body.users": {
|
||||
"name": "users",
|
||||
"description": "users",
|
||||
"required": false,
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/users"
|
||||
}
|
||||
},
|
||||
"rowFilter.users.id": {
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.users.username": {
|
||||
"name": "username",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.users.email": {
|
||||
"name": "email",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.users.password_hash": {
|
||||
"name": "password_hash",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"body.categories": {
|
||||
"name": "categories",
|
||||
"description": "categories",
|
||||
"required": false,
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/categories"
|
||||
}
|
||||
},
|
||||
"rowFilter.categories.id": {
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"format": "bigint",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"rowFilter.categories.name": {
|
||||
"name": "name",
|
||||
"required": false,
|
||||
"format": "text",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
"description": "PostgREST Documentation",
|
||||
"url": "https://postgrest.org/en/v12.2/api.html"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue