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
|
kind: Dashboard
|
||||||
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
|
||||||
version: v1alpha1
|
version: v1alpha1
|
||||||
|
webhooks:
|
||||||
|
defaulting: true
|
||||||
|
validation: true
|
||||||
|
webhookVersion: v1
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
11
Tiltfile
11
Tiltfile
|
@ -67,3 +67,14 @@ k8s_resource(
|
||||||
'supabase-controller-manager'
|
'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
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,3 +44,133 @@ type WorkloadTemplate struct {
|
||||||
// Workload - customize the container template of the workload
|
// Workload - customize the container template of the workload
|
||||||
Workload *ContainerTemplate `json:"workload,omitempty"`
|
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
|
// Secret - JWT HMAC secret in plain text
|
||||||
// This is WRITE-ONLY and will be copied to the SecretRef by the defaulter
|
// This is WRITE-ONLY and will be copied to the SecretRef by the defaulter
|
||||||
Secret *string `json:"secret,omitempty"`
|
Secret *string `json:"secret,omitempty"`
|
||||||
|
@ -113,7 +113,7 @@ type JwtSpec struct {
|
||||||
Expiry int `json:"expiry,omitempty"`
|
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
|
var secret corev1.Secret
|
||||||
if err := client.Get(ctx, types.NamespacedName{Name: s.SecretRef.Name}, &secret); err != nil {
|
if err := client.Get(ctx, types.NamespacedName{Name: s.SecretRef.Name}, &secret); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -127,21 +127,21 @@ func (s JwtSpec) GetJWTSecret(ctx context.Context, client client.Client) ([]byte
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s JwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
func (s CoreJwtSpec) SecretKeySelector() *corev1.SecretKeySelector {
|
||||||
return &corev1.SecretKeySelector{
|
return &corev1.SecretKeySelector{
|
||||||
LocalObjectReference: *s.SecretRef,
|
LocalObjectReference: *s.SecretRef,
|
||||||
Key: s.SecretKey,
|
Key: s.SecretKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s JwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
|
func (s CoreJwtSpec) JwksKeySelector() *corev1.SecretKeySelector {
|
||||||
return &corev1.SecretKeySelector{
|
return &corev1.SecretKeySelector{
|
||||||
LocalObjectReference: *s.SecretRef,
|
LocalObjectReference: *s.SecretRef,
|
||||||
Key: s.JwksKey,
|
Key: s.JwksKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s JwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
func (s CoreJwtSpec) SecretAsEnv(key string) corev1.EnvVar {
|
||||||
return corev1.EnvVar{
|
return corev1.EnvVar{
|
||||||
Name: key,
|
Name: key,
|
||||||
ValueFrom: &corev1.EnvVarSource{
|
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{
|
return corev1.EnvVar{
|
||||||
Name: key,
|
Name: key,
|
||||||
Value: strconv.Itoa(s.Expiry),
|
Value: strconv.Itoa(s.Expiry),
|
||||||
|
@ -358,12 +358,6 @@ func (p *AuthProviders) Vars(apiExternalURL string) []corev1.EnvVar {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthSpec struct {
|
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"`
|
AdditionalRedirectUrls []string `json:"additionalRedirectUrls,omitempty"`
|
||||||
DisableSignup *bool `json:"disableSignup,omitempty"`
|
DisableSignup *bool `json:"disableSignup,omitempty"`
|
||||||
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
|
AnonymousUsersEnabled *bool `json:"anonymousUsersEnabled,omitempty"`
|
||||||
|
@ -374,7 +368,13 @@ type AuthSpec struct {
|
||||||
|
|
||||||
// CoreSpec defines the desired state of Core.
|
// CoreSpec defines the desired state of Core.
|
||||||
type CoreSpec struct {
|
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"`
|
Database Database `json:"database,omitempty"`
|
||||||
Postgrest PostgrestSpec `json:"postgrest,omitempty"`
|
Postgrest PostgrestSpec `json:"postgrest,omitempty"`
|
||||||
Auth *AuthSpec `json:"auth,omitempty"`
|
Auth *AuthSpec `json:"auth,omitempty"`
|
||||||
|
|
|
@ -21,9 +21,53 @@ import (
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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 {
|
type StudioSpec struct {
|
||||||
|
JWT *DashboardJwtSpec `json:"jwt,omitempty"`
|
||||||
// WorkloadTemplate - customize the studio deployment
|
// WorkloadTemplate - customize the studio deployment
|
||||||
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
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 {
|
type PGMetaSpec struct {
|
||||||
|
@ -60,10 +104,8 @@ func (s DashboardDbSpec) PasswordRef() *corev1.SecretKeySelector {
|
||||||
type DashboardSpec struct {
|
type DashboardSpec struct {
|
||||||
DBSpec *DashboardDbSpec `json:"db"`
|
DBSpec *DashboardDbSpec `json:"db"`
|
||||||
// PGMeta
|
// PGMeta
|
||||||
// +kubebuilder:default={}
|
|
||||||
PGMeta *PGMetaSpec `json:"pgMeta,omitempty"`
|
PGMeta *PGMetaSpec `json:"pgMeta,omitempty"`
|
||||||
// Studio
|
// Studio
|
||||||
// +kubebuilder:default={}
|
|
||||||
Studio *StudioSpec `json:"studio,omitempty"`
|
Studio *StudioSpec `json:"studio,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -319,6 +319,31 @@ func (in *Core) DeepCopyObject() runtime.Object {
|
||||||
return nil
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *CoreList) DeepCopyInto(out *CoreList) {
|
func (in *CoreList) DeepCopyInto(out *CoreList) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -356,7 +381,7 @@ func (in *CoreSpec) DeepCopyInto(out *CoreSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.JWT != nil {
|
if in.JWT != nil {
|
||||||
in, out := &in.JWT, &out.JWT
|
in, out := &in.JWT, &out.JWT
|
||||||
*out = new(JwtSpec)
|
*out = new(CoreJwtSpec)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
in.Database.DeepCopyInto(&out.Database)
|
in.Database.DeepCopyInto(&out.Database)
|
||||||
|
@ -441,6 +466,26 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *DashboardList) DeepCopyInto(out *DashboardList) {
|
func (in *DashboardList) DeepCopyInto(out *DashboardList) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -751,31 +796,6 @@ func (in *ImageSpec) DeepCopy() *ImageSpec {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in MigrationStatus) DeepCopyInto(out *MigrationStatus) {
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
|
func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.JWT != nil {
|
||||||
|
in, out := &in.JWT, &out.JWT
|
||||||
|
*out = new(DashboardJwtSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
if in.WorkloadTemplate != nil {
|
if in.WorkloadTemplate != nil {
|
||||||
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
in, out := &in.WorkloadTemplate, &out.WorkloadTemplate
|
||||||
*out = new(WorkloadTemplate)
|
*out = new(WorkloadTemplate)
|
||||||
(*in).DeepCopyInto(*out)
|
(*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.
|
// 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)
|
return fmt.Errorf("unable to create controller Dashboard PG-Meta: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:goconst
|
if err = (&controller.DashboardStudioReconciler{
|
||||||
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
Client: mgr.GetClient(),
|
||||||
if err = webhooksupabasev1alpha1.SetupCoreWebhookWithManager(mgr); err != nil {
|
Scheme: mgr.GetScheme(),
|
||||||
return fmt.Errorf("unable to create webhook: %w", err)
|
}).SetupWithManager(mgr); err != nil {
|
||||||
}
|
return fmt.Errorf("unable to create controller Dashboard PG-Meta: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = (&controller.APIGatewayReconciler{
|
if err = (&controller.APIGatewayReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
}).SetupWithManager(ctx, mgr); err != nil {
|
}).SetupWithManager(ctx, mgr); err != nil {
|
||||||
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:goconst
|
// nolint:goconst
|
||||||
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
|
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 {
|
if err = webhooksupabasev1alpha1.SetupAPIGatewayWebhookWithManager(mgr, webhookConfig); err != nil {
|
||||||
setupLog.Error(err, "unable to create webhook", "webhook", "APIGateway")
|
setupLog.Error(err, "unable to create webhook", "webhook", "APIGateway")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = webhooksupabasev1alpha1.SetupDashboardWebhookWithManager(mgr); err != nil {
|
||||||
|
return fmt.Errorf("unable to create webhook: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:scaffold:builder
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
|
|
@ -51,11 +51,6 @@ spec:
|
||||||
type: boolean
|
type: boolean
|
||||||
emailSignupDisabled:
|
emailSignupDisabled:
|
||||||
type: boolean
|
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:
|
providers:
|
||||||
properties:
|
properties:
|
||||||
azure:
|
azure:
|
||||||
|
@ -191,11 +186,6 @@ spec:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
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:
|
workloadTemplate:
|
||||||
properties:
|
properties:
|
||||||
additionalLabels:
|
additionalLabels:
|
||||||
|
@ -883,9 +873,6 @@ spec:
|
||||||
required:
|
required:
|
||||||
- securityContext
|
- securityContext
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- externalUrl
|
|
||||||
- siteUrl
|
|
||||||
type: object
|
type: object
|
||||||
database:
|
database:
|
||||||
properties:
|
properties:
|
||||||
|
@ -1010,6 +997,11 @@ spec:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
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:
|
jwt:
|
||||||
properties:
|
properties:
|
||||||
anonKey:
|
anonKey:
|
||||||
|
@ -1775,6 +1767,14 @@ spec:
|
||||||
- securityContext
|
- securityContext
|
||||||
type: object
|
type: object
|
||||||
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
|
type: object
|
||||||
status:
|
status:
|
||||||
description: CoreStatus defines the observed state of Core.
|
description: CoreStatus defines the observed state of Core.
|
||||||
|
|
|
@ -71,7 +71,6 @@ spec:
|
||||||
- host
|
- host
|
||||||
type: object
|
type: object
|
||||||
pgMeta:
|
pgMeta:
|
||||||
default: {}
|
|
||||||
description: PGMeta
|
description: PGMeta
|
||||||
properties:
|
properties:
|
||||||
workloadTemplate:
|
workloadTemplate:
|
||||||
|
@ -764,9 +763,57 @@ spec:
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
studio:
|
studio:
|
||||||
default: {}
|
|
||||||
description: Studio
|
description: Studio
|
||||||
properties:
|
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:
|
workloadTemplate:
|
||||||
description: WorkloadTemplate - customize the studio deployment
|
description: WorkloadTemplate - customize the studio deployment
|
||||||
properties:
|
properties:
|
||||||
|
@ -1455,6 +1502,8 @@ spec:
|
||||||
required:
|
required:
|
||||||
- securityContext
|
- securityContext
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- externalUrl
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- db
|
- db
|
||||||
|
|
|
@ -13,13 +13,19 @@ metadata:
|
||||||
app.kubernetes.io/managed-by: kustomize
|
app.kubernetes.io/managed-by: kustomize
|
||||||
name: core-sample
|
name: core-sample
|
||||||
spec:
|
spec:
|
||||||
|
externalUrl: http://localhost:8000/
|
||||||
|
siteUrl: http://localhost:3000/
|
||||||
database:
|
database:
|
||||||
dsnFrom:
|
dsnFrom:
|
||||||
name: supabase-demo-credentials
|
name: supabase-demo-credentials
|
||||||
key: url
|
key: url
|
||||||
auth:
|
auth:
|
||||||
externalUrl: http://localhost:8000/
|
|
||||||
siteUrl: http://localhost:3000/
|
|
||||||
disableSignup: true
|
disableSignup: true
|
||||||
enableEmailAutoconfirm: true
|
enableEmailAutoconfirm: true
|
||||||
providers: {}
|
providers: {}
|
||||||
|
postgrest:
|
||||||
|
maxRows: 1000
|
||||||
|
jwt:
|
||||||
|
expiry: 3600
|
||||||
|
secretRef:
|
||||||
|
name: core-sample-jwt
|
||||||
|
|
|
@ -7,7 +7,15 @@ metadata:
|
||||||
name: core-sample
|
name: core-sample
|
||||||
spec:
|
spec:
|
||||||
db:
|
db:
|
||||||
host: cluster-example-rw.supabase-demo
|
host: cluster-example-rw.supabase-demo.svc
|
||||||
dbName: app
|
dbName: app
|
||||||
dbCredentialsRef:
|
dbCredentialsRef:
|
||||||
name: db-roles-creds-supabase-admin
|
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:
|
resources:
|
||||||
- cores
|
- cores
|
||||||
sideEffects: None
|
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
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
kind: ValidatingWebhookConfiguration
|
kind: ValidatingWebhookConfiguration
|
||||||
|
@ -90,3 +110,23 @@ webhooks:
|
||||||
resources:
|
resources:
|
||||||
- cores
|
- cores
|
||||||
sideEffects: None
|
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 |
|
| 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_ | | | |
|
| `additionalRedirectUrls` _string array_ | | | |
|
||||||
| `disableSignup` _boolean_ | | | |
|
| `disableSignup` _boolean_ | | | |
|
||||||
| `anonymousUsersEnabled` _boolean_ | | | |
|
| `anonymousUsersEnabled` _boolean_ | | | |
|
||||||
|
@ -212,7 +210,9 @@ _Appears in:_
|
||||||
| `spec` _[CoreSpec](#corespec)_ | | | |
|
| `spec` _[CoreSpec](#corespec)_ | | | |
|
||||||
|
|
||||||
|
|
||||||
#### CoreCondition
|
|
||||||
|
|
||||||
|
#### CoreJwtSpec
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -221,28 +221,17 @@ _Appears in:_
|
||||||
|
|
||||||
|
|
||||||
_Appears in:_
|
_Appears in:_
|
||||||
- [CoreStatus](#corestatus)
|
- [CoreSpec](#corespec)
|
||||||
|
|
||||||
| Field | Description | Default | Validation |
|
| Field | Description | Default | Validation |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `type` _[CoreConditionType](#coreconditiontype)_ | | | |
|
| `secret` _string_ | Secret - JWT HMAC secret in plain text<br />This is WRITE-ONLY and will be copied to the SecretRef by the defaulter | | |
|
||||||
| `lastProbeTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
| `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 | | |
|
||||||
| `lastTransitionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#time-v1-meta)_ | | | |
|
| `secretKey` _string_ | SecretKey - key in secret where to read the JWT HMAC secret from | secret | |
|
||||||
| `reason` _string_ | | | |
|
| `jwksKey` _string_ | JwksKey - key in secret where to read the JWKS from | jwks.json | |
|
||||||
| `message` _string_ | | | |
|
| `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 | |
|
||||||
#### CoreConditionType
|
|
||||||
|
|
||||||
_Underlying type:_ _string_
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_Appears in:_
|
|
||||||
- [CoreCondition](#corecondition)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### CoreList
|
#### CoreList
|
||||||
|
@ -276,7 +265,9 @@ _Appears in:_
|
||||||
|
|
||||||
| Field | Description | Default | Validation |
|
| 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)_ | | | |
|
| `database` _[Database](#database)_ | | | |
|
||||||
| `postgrest` _[PostgrestSpec](#postgrestspec)_ | | | |
|
| `postgrest` _[PostgrestSpec](#postgrestspec)_ | | | |
|
||||||
| `auth` _[AuthSpec](#authspec)_ | | | |
|
| `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 | | |
|
| `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
|
#### DashboardList
|
||||||
|
|
||||||
|
|
||||||
|
@ -354,8 +364,8 @@ _Appears in:_
|
||||||
| Field | Description | Default | Validation |
|
| Field | Description | Default | Validation |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `db` _[DashboardDbSpec](#dashboarddbspec)_ | | | |
|
| `db` _[DashboardDbSpec](#dashboarddbspec)_ | | | |
|
||||||
| `pgMeta` _[PGMetaSpec](#pgmetaspec)_ | PGMeta | \{ \} | |
|
| `pgMeta` _[PGMetaSpec](#pgmetaspec)_ | PGMeta | | |
|
||||||
| `studio` _[StudioSpec](#studiospec)_ | Studio | \{ \} | |
|
| `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)_ | | | |
|
| `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
|
#### 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 |
|
| Field | Description | Default | Validation |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
| `jwt` _[DashboardJwtSpec](#dashboardjwtspec)_ | | | |
|
||||||
| `workloadTemplate` _[WorkloadTemplate](#workloadtemplate)_ | WorkloadTemplate - customize the studio deployment | | |
|
| `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
|
#### WorkloadTemplate
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -13,6 +13,7 @@ require (
|
||||||
github.com/onsi/ginkgo/v2 v2.19.0
|
github.com/onsi/ginkgo/v2 v2.19.0
|
||||||
github.com/onsi/gomega v1.33.1
|
github.com/onsi/gomega v1.33.1
|
||||||
go.uber.org/zap v1.26.0
|
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/grpc v1.65.0
|
||||||
google.golang.org/protobuf v1.34.2
|
google.golang.org/protobuf v1.34.2
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
@ -94,7 +95,6 @@ require (
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.29.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/net v0.26.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sync v0.9.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 {
|
_, 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 {
|
type nodeSpec struct {
|
||||||
Cluster string
|
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 (
|
var (
|
||||||
image = supabase.Images.Envoy.String()
|
envoySpec = gateway.Spec.Envoy
|
||||||
podSecurityContext = envoySpec.WorkloadTemplate.SecurityContext
|
serviceCfg = supabase.ServiceConfig.Envoy
|
||||||
pullPolicy = envoySpec.WorkloadTemplate.Workload.PullPolicy
|
|
||||||
containerSecurityContext = envoySpec.WorkloadTemplate.Workload.SecurityContext
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyDeployment, func() error {
|
||||||
envoyDeployment.Labels = MergeLabels(
|
envoyDeployment.Labels = envoySpec.WorkloadTemplate.MergeLabels(
|
||||||
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Postgrest.Tag),
|
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||||
gateway.Labels,
|
gateway.Labels,
|
||||||
envoySpec.WorkloadTemplate.AdditionalLabels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if envoyDeployment.CreationTimestamp.IsZero() {
|
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{
|
envoyDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
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, "config-hash"): configHash,
|
||||||
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwks-hash"): jwksHash,
|
fmt.Sprintf("%s/%s", supabasev1alpha1.GroupVersion.Group, "jwks-hash"): jwksHash,
|
||||||
},
|
},
|
||||||
Labels: MergeLabels(
|
Labels: objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
||||||
objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag),
|
|
||||||
envoySpec.WorkloadTemplate.AdditionalLabels,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
ImagePullSecrets: envoySpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
ImagePullSecrets: envoySpec.WorkloadTemplate.PullSecrets(),
|
||||||
AutomountServiceAccountToken: ptrOf(false),
|
AutomountServiceAccountToken: ptrOf(false),
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: "envoy-proxy",
|
Name: "envoy-proxy",
|
||||||
Image: image,
|
Image: envoySpec.WorkloadTemplate.Image(supabase.Images.Envoy.String()),
|
||||||
ImagePullPolicy: pullPolicy,
|
ImagePullPolicy: envoySpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Args: []string{"-c /etc/envoy/config.yaml"},
|
Args: []string{"-c /etc/envoy/config.yaml"},
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
|
@ -362,18 +316,18 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: envoySpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
||||||
Resources: envoySpec.WorkloadTemplate.Workload.Resources,
|
Resources: envoySpec.WorkloadTemplate.Resources(),
|
||||||
VolumeMounts: []corev1.VolumeMount{
|
VolumeMounts: envoySpec.WorkloadTemplate.AdditionalVolumeMounts(
|
||||||
{
|
corev1.VolumeMount{
|
||||||
Name: "config",
|
Name: "config",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
MountPath: "/etc/envoy",
|
MountPath: "/etc/envoy",
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SecurityContext: podSecurityContext,
|
SecurityContext: envoySpec.WorkloadTemplate.PodSecurityContext(),
|
||||||
Volumes: []corev1.Volume{
|
Volumes: []corev1.Volume{
|
||||||
{
|
{
|
||||||
Name: "config",
|
Name: "config",
|
||||||
|
@ -432,10 +386,10 @@ func (r *APIGatewayReconciler) reconcileEnvoyService(
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyService, func() error {
|
_, 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{
|
envoyService.Spec = corev1.ServiceSpec{
|
||||||
Selector: selectorLabels(gateway, "postgrest"),
|
Selector: selectorLabels(gateway, "envoy"),
|
||||||
Ports: []corev1.ServicePort{
|
Ports: []corev1.ServicePort{
|
||||||
{
|
{
|
||||||
Name: "rest",
|
Name: "rest",
|
||||||
|
|
|
@ -72,52 +72,11 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
authDeployment = &appsv1.Deployment{
|
authDeployment = &appsv1.Deployment{
|
||||||
ObjectMeta: supabase.ServiceConfig.Auth.ObjectMeta(core),
|
ObjectMeta: supabase.ServiceConfig.Auth.ObjectMeta(core),
|
||||||
}
|
}
|
||||||
authSpec = core.Spec.Auth
|
authSpec = core.Spec.Auth
|
||||||
svcCfg = supabase.ServiceConfig.Auth
|
svcCfg = supabase.ServiceConfig.Auth
|
||||||
|
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -129,7 +88,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, authDeployment, func() error {
|
_, 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),
|
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
core.Labels,
|
core.Labels,
|
||||||
)
|
)
|
||||||
|
@ -153,24 +112,24 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
}
|
}
|
||||||
|
|
||||||
authEnv := append(authDbEnv,
|
authEnv := append(authDbEnv,
|
||||||
svcCfg.EnvKeys.ApiHost.Var(svcCfg.Defaults.ApiHost),
|
svcCfg.EnvKeys.ApiHost.Var(),
|
||||||
svcCfg.EnvKeys.ApiPort.Var(svcCfg.Defaults.ApiPort),
|
svcCfg.EnvKeys.ApiPort.Var(),
|
||||||
svcCfg.EnvKeys.ApiExternalUrl.Var(authSpec.APIExternalURL),
|
svcCfg.EnvKeys.ApiExternalUrl.Var(core.Spec.APIExternalURL),
|
||||||
svcCfg.EnvKeys.DBDriver.Var(svcCfg.Defaults.DbDriver),
|
svcCfg.EnvKeys.DBDriver.Var(),
|
||||||
svcCfg.EnvKeys.SiteUrl.Var(authSpec.SiteURL),
|
svcCfg.EnvKeys.SiteUrl.Var(core.Spec.SiteURL),
|
||||||
svcCfg.EnvKeys.AdditionalRedirectURLs.Var(authSpec.AdditionalRedirectUrls),
|
svcCfg.EnvKeys.AdditionalRedirectURLs.Var(authSpec.AdditionalRedirectUrls),
|
||||||
svcCfg.EnvKeys.DisableSignup.Var(boolValueOf(authSpec.DisableSignup)),
|
svcCfg.EnvKeys.DisableSignup.Var(boolValueOf(authSpec.DisableSignup)),
|
||||||
svcCfg.EnvKeys.JWTIssuer.Var(svcCfg.Defaults.JwtIssuer),
|
svcCfg.EnvKeys.JWTIssuer.Var(),
|
||||||
svcCfg.EnvKeys.JWTAdminRoles.Var(svcCfg.Defaults.JwtAdminRoles),
|
svcCfg.EnvKeys.JWTAdminRoles.Var(),
|
||||||
svcCfg.EnvKeys.JWTAudience.Var(svcCfg.Defaults.JwtAudience),
|
svcCfg.EnvKeys.JWTAudience.Var(),
|
||||||
svcCfg.EnvKeys.JwtDefaultGroup.Var(svcCfg.Defaults.JwtDefaultGroupName),
|
svcCfg.EnvKeys.JwtDefaultGroup.Var(),
|
||||||
svcCfg.EnvKeys.JwtExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
svcCfg.EnvKeys.JwtExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
||||||
svcCfg.EnvKeys.JwtSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
svcCfg.EnvKeys.JwtSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||||
svcCfg.EnvKeys.EmailSignupDisabled.Var(boolValueOf(authSpec.EmailSignupDisabled)),
|
svcCfg.EnvKeys.EmailSignupDisabled.Var(boolValueOf(authSpec.EmailSignupDisabled)),
|
||||||
svcCfg.EnvKeys.AnonymousUsersEnabled.Var(boolValueOf(authSpec.AnonymousUsersEnabled)),
|
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() {
|
if authDeployment.CreationTimestamp.IsZero() {
|
||||||
authDeployment.Spec.Selector = &metav1.LabelSelector{
|
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{
|
authDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Labels: objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
Labels: objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
ImagePullSecrets: authSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
ImagePullSecrets: authSpec.WorkloadTemplate.PullSecrets(),
|
||||||
InitContainers: []corev1.Container{{
|
InitContainers: []corev1.Container{{
|
||||||
Name: "migrations",
|
Name: "supabase-auth-migrations",
|
||||||
Image: image,
|
Image: authSpec.WorkloadTemplate.Image(supabase.Images.Gotrue.String()),
|
||||||
ImagePullPolicy: pullPolicy,
|
ImagePullPolicy: authSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Command: []string{"/usr/local/bin/auth"},
|
Command: []string{"/usr/local/bin/auth"},
|
||||||
Args: []string{"migrate"},
|
Args: []string{"migrate"},
|
||||||
Env: authEnv,
|
Env: authSpec.WorkloadTemplate.MergeEnv(authEnv),
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: authSpec.WorkloadTemplate.ContainerSecurityContext(svcCfg.Defaults.UID, svcCfg.Defaults.GID),
|
||||||
}},
|
}},
|
||||||
Containers: []corev1.Container{{
|
Containers: []corev1.Container{{
|
||||||
Name: "supabase-auth",
|
Name: "supabase-auth",
|
||||||
Image: image,
|
Image: authSpec.WorkloadTemplate.Image(supabase.Images.Gotrue.String()),
|
||||||
ImagePullPolicy: pullPolicy,
|
ImagePullPolicy: authSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Command: []string{"/usr/local/bin/auth"},
|
Command: []string{"/usr/local/bin/auth"},
|
||||||
Args: []string{"serve"},
|
Args: []string{"serve"},
|
||||||
Env: MergeEnv(authEnv, authSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
Env: authSpec.WorkloadTemplate.MergeEnv(authEnv),
|
||||||
Ports: []corev1.ContainerPort{{
|
Ports: []corev1.ContainerPort{{
|
||||||
Name: "api",
|
Name: "api",
|
||||||
ContainerPort: 9999,
|
ContainerPort: svcCfg.Defaults.APIPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
}},
|
}},
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: authSpec.WorkloadTemplate.ContainerSecurityContext(svcCfg.Defaults.UID, svcCfg.Defaults.GID),
|
||||||
Resources: authSpec.WorkloadTemplate.Workload.Resources,
|
Resources: authSpec.WorkloadTemplate.Resources(),
|
||||||
VolumeMounts: authSpec.WorkloadTemplate.Workload.VolumeMounts,
|
VolumeMounts: authSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||||
ReadinessProbe: &corev1.Probe{
|
ReadinessProbe: &corev1.Probe{
|
||||||
InitialDelaySeconds: 5,
|
InitialDelaySeconds: 5,
|
||||||
PeriodSeconds: 3,
|
PeriodSeconds: 3,
|
||||||
|
@ -218,7 +177,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
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{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
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 {
|
_, 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),
|
objectLabels(core, "auth", "core", supabase.Images.Gotrue.Tag),
|
||||||
core.Labels,
|
core.Labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
authService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
if _, ok := authService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||||
|
authService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||||
|
}
|
||||||
|
|
||||||
authService.Spec = corev1.ServiceSpec{
|
authService.Spec = corev1.ServiceSpec{
|
||||||
Selector: selectorLabels(core, "auth"),
|
Selector: selectorLabels(core, "auth"),
|
||||||
|
|
|
@ -78,51 +78,13 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
postgrestSpec = core.Spec.Postgrest
|
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 (
|
var (
|
||||||
image = supabase.Images.Postgrest.String()
|
anonRole = ValueOrFallback(postgrestSpec.AnonRole, serviceCfg.Defaults.AnonRole)
|
||||||
podSecurityContext = postgrestSpec.WorkloadTemplate.SecurityContext
|
postgrestSchemas = ValueOrFallback(postgrestSpec.Schemas, serviceCfg.Defaults.Schemas)
|
||||||
pullPolicy = postgrestSpec.WorkloadTemplate.Workload.PullPolicy
|
jwtSecretHash string
|
||||||
containerSecurityContext = postgrestSpec.WorkloadTemplate.Workload.SecurityContext
|
namespacedClient = client.NewNamespacedClient(r.Client, core.Namespace)
|
||||||
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)
|
databaseDSN, err := core.Spec.Database.GetDSN(ctx, namespacedClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -140,7 +102,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, postgrestDeployment, func() error {
|
_, 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),
|
objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
core.Labels,
|
core.Labels,
|
||||||
)
|
)
|
||||||
|
@ -161,6 +123,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
Name: serviceCfg.EnvKeys.DBUri,
|
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()), "?"),
|
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.JWTSecret.Var(core.Spec.JWT.JwksKeySelector()),
|
||||||
serviceCfg.EnvKeys.Schemas.Var(postgrestSchemas),
|
serviceCfg.EnvKeys.Schemas.Var(postgrestSchemas),
|
||||||
serviceCfg.EnvKeys.AnonRole.Var(anonRole),
|
serviceCfg.EnvKeys.AnonRole.Var(anonRole),
|
||||||
|
@ -168,7 +131,9 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
serviceCfg.EnvKeys.ExtraSearchPath.Var(serviceCfg.Defaults.ExtraSearchPath),
|
serviceCfg.EnvKeys.ExtraSearchPath.Var(serviceCfg.Defaults.ExtraSearchPath),
|
||||||
serviceCfg.EnvKeys.AppSettingsJWTSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
serviceCfg.EnvKeys.AppSettingsJWTSecret.Var(core.Spec.JWT.SecretKeySelector()),
|
||||||
serviceCfg.EnvKeys.AppSettingsJWTExpiry.Var(ValueOrFallback(core.Spec.JWT.Expiry, supabase.ServiceConfig.JWT.Defaults.Expiry)),
|
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() {
|
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{
|
postgrestDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
@ -187,29 +152,29 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
Labels: objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
Labels: objectLabels(core, serviceCfg.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
ImagePullSecrets: postgrestSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
ImagePullSecrets: postgrestSpec.WorkloadTemplate.PullSecrets(),
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: "supabase-rest",
|
Name: "supabase-rest",
|
||||||
Image: image,
|
Image: postgrestSpec.WorkloadTemplate.Image(supabase.Images.Postgrest.String()),
|
||||||
ImagePullPolicy: pullPolicy,
|
ImagePullPolicy: postgrestSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Args: []string{"postgrest"},
|
Args: []string{"postgrest"},
|
||||||
Env: MergeEnv(postgrestEnv, postgrestSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
Env: postgrestSpec.WorkloadTemplate.MergeEnv(postgrestEnv),
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
Name: "rest",
|
Name: "rest",
|
||||||
ContainerPort: 3000,
|
ContainerPort: serviceCfg.Defaults.ServerPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "admin",
|
Name: "admin",
|
||||||
ContainerPort: 3001,
|
ContainerPort: serviceCfg.Defaults.AdminPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: postgrestSpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
||||||
Resources: postgrestSpec.WorkloadTemplate.Workload.Resources,
|
Resources: postgrestSpec.WorkloadTemplate.Resources(),
|
||||||
VolumeMounts: postgrestSpec.WorkloadTemplate.Workload.VolumeMounts,
|
VolumeMounts: postgrestSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||||
ReadinessProbe: &corev1.Probe{
|
ReadinessProbe: &corev1.Probe{
|
||||||
InitialDelaySeconds: 5,
|
InitialDelaySeconds: 5,
|
||||||
PeriodSeconds: 3,
|
PeriodSeconds: 3,
|
||||||
|
@ -218,7 +183,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/ready",
|
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{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/live",
|
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 {
|
_, 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),
|
objectLabels(core, supabase.ServiceConfig.Postgrest.Name, "core", supabase.Images.Postgrest.Tag),
|
||||||
core.Labels,
|
core.Labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
if _, ok := postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
|
||||||
|
postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
|
||||||
|
}
|
||||||
|
|
||||||
postgrestService.Spec = corev1.ServiceSpec{
|
postgrestService.Spec = corev1.ServiceSpec{
|
||||||
Selector: selectorLabels(core, supabase.ServiceConfig.Postgrest.Name),
|
Selector: selectorLabels(core, supabase.ServiceConfig.Postgrest.Name),
|
||||||
|
|
|
@ -40,19 +40,6 @@ type DashboardPGMetaReconciler struct {
|
||||||
Scheme *runtime.Scheme
|
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) {
|
func (r *DashboardPGMetaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
var (
|
var (
|
||||||
dashboard supabasev1alpha1.Dashboard
|
dashboard supabasev1alpha1.Dashboard
|
||||||
|
@ -101,47 +88,6 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
pgMetaSpec = new(supabasev1alpha1.PGMetaSpec)
|
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{
|
dsnSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: dashboard.Spec.DBSpec.DBCredentialsRef.Name,
|
Name: dashboard.Spec.DBSpec.DBCredentialsRef.Name,
|
||||||
|
@ -153,7 +99,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, pgMetaDeployment, func() error {
|
_, 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),
|
objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
dashboard.Labels,
|
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{
|
pgMetaEnv := []corev1.EnvVar{
|
||||||
serviceCfg.EnvKeys.APIPort.Var(serviceCfg.Defaults.APIPort),
|
serviceCfg.EnvKeys.APIPort.Var(serviceCfg.Defaults.APIPort),
|
||||||
serviceCfg.EnvKeys.DBHost.Var(dashboard.Spec.DBSpec.Host),
|
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.DBPort.Var(dashboard.Spec.DBSpec.Port),
|
||||||
serviceCfg.EnvKeys.DBUser.Var(dashboard.Spec.DBSpec.UserRef()),
|
serviceCfg.EnvKeys.DBUser.Var(dashboard.Spec.DBSpec.UserRef()),
|
||||||
serviceCfg.EnvKeys.DBPassword.Var(dashboard.Spec.DBSpec.PasswordRef()),
|
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),
|
Labels: objectLabels(dashboard, serviceCfg.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
},
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
ImagePullSecrets: pgMetaSpec.WorkloadTemplate.Workload.ImagePullSecrets,
|
ImagePullSecrets: pgMetaSpec.WorkloadTemplate.PullSecrets(),
|
||||||
Containers: []corev1.Container{{
|
Containers: []corev1.Container{{
|
||||||
Name: "supabase-meta",
|
Name: "supabase-meta",
|
||||||
Image: image,
|
Image: pgMetaSpec.WorkloadTemplate.Image(supabase.Images.PostgresMeta.String()),
|
||||||
ImagePullPolicy: pullPolicy,
|
ImagePullPolicy: pgMetaSpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Env: MergeEnv(pgMetaEnv, pgMetaSpec.WorkloadTemplate.Workload.AdditionalEnv...),
|
Env: pgMetaSpec.WorkloadTemplate.MergeEnv(pgMetaEnv),
|
||||||
Ports: []corev1.ContainerPort{{
|
Ports: []corev1.ContainerPort{{
|
||||||
Name: "api",
|
Name: "api",
|
||||||
ContainerPort: int32(serviceCfg.Defaults.APIPort),
|
ContainerPort: serviceCfg.Defaults.APIPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
}},
|
}},
|
||||||
SecurityContext: containerSecurityContext,
|
SecurityContext: pgMetaSpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.NodeUID, serviceCfg.Defaults.NodeGID),
|
||||||
Resources: pgMetaSpec.WorkloadTemplate.Workload.Resources,
|
Resources: pgMetaSpec.WorkloadTemplate.Resources(),
|
||||||
VolumeMounts: pgMetaSpec.WorkloadTemplate.Workload.VolumeMounts,
|
VolumeMounts: pgMetaSpec.WorkloadTemplate.AdditionalVolumeMounts(),
|
||||||
ReadinessProbe: &corev1.Probe{
|
ReadinessProbe: &corev1.Probe{
|
||||||
InitialDelaySeconds: 5,
|
InitialDelaySeconds: 5,
|
||||||
PeriodSeconds: 3,
|
PeriodSeconds: 3,
|
||||||
|
@ -201,7 +148,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
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{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
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),
|
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 {
|
_, 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),
|
objectLabels(dashboard, supabase.ServiceConfig.PGMeta.Name, "dashboard", supabase.Images.PostgresMeta.Tag),
|
||||||
dashboard.Labels,
|
dashboard.Labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
|
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{
|
pgMetaService.Spec = corev1.ServiceSpec{
|
||||||
Selector: selectorLabels(dashboard, supabase.ServiceConfig.PGMeta.Name),
|
Selector: selectorLabels(dashboard, supabase.ServiceConfig.PGMeta.Name),
|
||||||
|
@ -256,8 +207,8 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaService(
|
||||||
Name: "api",
|
Name: "api",
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
AppProtocol: ptrOf("http"),
|
AppProtocol: ptrOf("http"),
|
||||||
Port: apiPort,
|
Port: supabase.ServiceConfig.PGMeta.Defaults.APIPort,
|
||||||
TargetPort: intstr.IntOrString{IntVal: 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,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/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=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=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=secrets;configmaps;services,verbs=get;list;watch;create;update;patch;delete
|
||||||
// +kubebuilder:rbac:groups="",resources=events,verbs=create
|
// +kubebuilder:rbac:groups="",resources=events,verbs=create
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
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"
|
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/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"
|
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||||
|
@ -176,6 +176,7 @@ type envoyClusterServices struct {
|
||||||
Postgrest *PostgrestCluster
|
Postgrest *PostgrestCluster
|
||||||
GoTrue *GoTrueCluster
|
GoTrue *GoTrueCluster
|
||||||
PGMeta *PGMetaCluster
|
PGMeta *PGMetaCluster
|
||||||
|
Studio *StudioCluster
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *envoyClusterServices) UpsertEndpoints(eps *discoveryv1.EndpointSlice) {
|
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) {
|
func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapshot, error) {
|
||||||
const (
|
const (
|
||||||
routeName = "supabase"
|
apiRouteName = "supabase"
|
||||||
vHostName = "supabase"
|
studioRouteName = "supabas-studio"
|
||||||
listenerName = "supabase"
|
vHostName = "supabase"
|
||||||
|
listenerName = "supabase"
|
||||||
)
|
)
|
||||||
|
|
||||||
manager := &hcm.HttpConnectionManager{
|
apiConnectionManager := &hcm.HttpConnectionManager{
|
||||||
CodecType: hcm.HttpConnectionManager_AUTO,
|
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||||
StatPrefix: "http",
|
StatPrefix: "http",
|
||||||
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||||
|
@ -225,7 +227,7 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
RouteConfigName: routeName,
|
RouteConfigName: apiRouteName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpFilters: []*hcm.HttpFilter{
|
HttpFilters: []*hcm.HttpFilter{
|
||||||
|
@ -244,8 +246,39 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
routeCfg := &route.RouteConfiguration{
|
studioConnetionManager := &hcm.HttpConnectionManager{
|
||||||
Name: routeName,
|
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{{
|
VirtualHosts: []*route.VirtualHost{{
|
||||||
Name: "supabase",
|
Name: "supabase",
|
||||||
Domains: []string{"*"},
|
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,
|
Name: listenerName,
|
||||||
Address: &corev3.Address{
|
Address: &corev3.Address{
|
||||||
Address: &corev3.Address_SocketAddress{
|
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,
|
Name: FilterNameHttpConnectionManager,
|
||||||
ConfigType: &listener.Filter_TypedConfig{
|
ConfigType: &listenerv3.Filter_TypedConfig{
|
||||||
TypedConfig: MustAny(manager),
|
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{
|
rawSnapshot := map[resource.Type][]types.Resource{
|
||||||
|
@ -298,8 +362,8 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
|
||||||
s.GoTrue.Cluster(instance),
|
s.GoTrue.Cluster(instance),
|
||||||
s.PGMeta.Cluster(instance),
|
s.PGMeta.Cluster(instance),
|
||||||
)...),
|
)...),
|
||||||
resource.RouteType: {routeCfg},
|
resource.RouteType: {apiRouteCfg},
|
||||||
resource.ListenerType: {listener},
|
resource.ListenerType: castResources(listeners...),
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot, err := cache.NewSnapshot(
|
snapshot, err := cache.NewSnapshot(
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (c *PostgrestCluster) Routes(instance string) []*routev3.Route {
|
||||||
Name: "PostgREST: /rest/v1/* -> http://rest:3000/*",
|
Name: "PostgREST: /rest/v1/* -> http://rest:3000/*",
|
||||||
Match: &routev3.RouteMatch{
|
Match: &routev3.RouteMatch{
|
||||||
PathSpecifier: &routev3.RouteMatch_Prefix{
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
Prefix: "/rest/v1",
|
Prefix: "/rest/v1/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: &routev3.Route_Route{
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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 {
|
type serviceConfig[TEnvKeys, TDefaults any] struct {
|
||||||
Name string
|
Name string
|
||||||
EnvKeys TEnvKeys
|
EnvKeys TEnvKeys
|
||||||
|
@ -74,6 +21,7 @@ func (cfg serviceConfig[TEnvKeys, TDefaults]) ObjectMeta(obj metav1.Object) meta
|
||||||
}
|
}
|
||||||
|
|
||||||
type postgrestEnvKeys struct {
|
type postgrestEnvKeys struct {
|
||||||
|
Host fixedEnv
|
||||||
DBUri string
|
DBUri string
|
||||||
Schemas stringSliceEnv
|
Schemas stringSliceEnv
|
||||||
AnonRole stringEnv
|
AnonRole stringEnv
|
||||||
|
@ -81,31 +29,34 @@ type postgrestEnvKeys struct {
|
||||||
UseLegacyGucs boolEnv
|
UseLegacyGucs boolEnv
|
||||||
ExtraSearchPath stringSliceEnv
|
ExtraSearchPath stringSliceEnv
|
||||||
AppSettingsJWTSecret secretEnv
|
AppSettingsJWTSecret secretEnv
|
||||||
AppSettingsJWTExpiry intEnv
|
AppSettingsJWTExpiry intEnv[int]
|
||||||
AdminServerPort intEnv
|
AdminServerPort intEnv[int32]
|
||||||
MaxRows intEnv
|
MaxRows intEnv[int]
|
||||||
|
OpenAPIProxyURI stringEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
type postgrestConfigDefaults struct {
|
type postgrestConfigDefaults struct {
|
||||||
AnonRole string
|
AnonRole string
|
||||||
Schemas []string
|
Schemas []string
|
||||||
ExtraSearchPath []string
|
ExtraSearchPath []string
|
||||||
|
UID, GID int64
|
||||||
|
ServerPort, AdminPort int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type authEnvKeys struct {
|
type authEnvKeys struct {
|
||||||
ApiHost stringEnv
|
ApiHost fixedEnv
|
||||||
ApiPort intEnv
|
ApiPort fixedEnv
|
||||||
ApiExternalUrl stringEnv
|
ApiExternalUrl stringEnv
|
||||||
DBDriver stringEnv
|
DBDriver fixedEnv
|
||||||
DatabaseUrl string
|
DatabaseUrl string
|
||||||
SiteUrl stringEnv
|
SiteUrl stringEnv
|
||||||
AdditionalRedirectURLs stringSliceEnv
|
AdditionalRedirectURLs stringSliceEnv
|
||||||
DisableSignup boolEnv
|
DisableSignup boolEnv
|
||||||
JWTIssuer stringEnv
|
JWTIssuer fixedEnv
|
||||||
JWTAdminRoles stringEnv
|
JWTAdminRoles fixedEnv
|
||||||
JWTAudience stringEnv
|
JWTAudience fixedEnv
|
||||||
JwtDefaultGroup stringEnv
|
JwtDefaultGroup fixedEnv
|
||||||
JwtExpiry intEnv
|
JwtExpiry intEnv[int]
|
||||||
JwtSecret secretEnv
|
JwtSecret secretEnv
|
||||||
EmailSignupDisabled boolEnv
|
EmailSignupDisabled boolEnv
|
||||||
MailerUrlPathsInvite stringEnv
|
MailerUrlPathsInvite stringEnv
|
||||||
|
@ -116,35 +67,50 @@ type authEnvKeys struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type authConfigDefaults struct {
|
type authConfigDefaults struct {
|
||||||
ApiHost string
|
|
||||||
ApiPort int
|
|
||||||
DbDriver string
|
|
||||||
JwtIssuer string
|
|
||||||
JwtAdminRoles string
|
|
||||||
JwtAudience string
|
|
||||||
JwtDefaultGroupName string
|
|
||||||
MailerUrlPathsInvite string
|
MailerUrlPathsInvite string
|
||||||
MailerUrlPathsConfirmation string
|
MailerUrlPathsConfirmation string
|
||||||
MailerUrlPathsRecovery string
|
MailerUrlPathsRecovery string
|
||||||
MailerUrlPathsEmailChange string
|
MailerUrlPathsEmailChange string
|
||||||
|
APIPort int32
|
||||||
|
UID, GID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type pgMetaEnvKeys struct {
|
type pgMetaEnvKeys struct {
|
||||||
APIPort intEnv
|
APIPort intEnv[int32]
|
||||||
DBHost stringEnv
|
DBHost stringEnv
|
||||||
DBPort intEnv
|
DBPort intEnv[int]
|
||||||
DBName stringEnv
|
DBName stringEnv
|
||||||
DBUser secretEnv
|
DBUser secretEnv
|
||||||
DBPassword secretEnv
|
DBPassword secretEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
type pgMetaDefaults struct {
|
type pgMetaDefaults struct {
|
||||||
APIPort int
|
APIPort int32
|
||||||
DBPort string
|
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 {
|
type envoyDefaults struct {
|
||||||
ConfigKey string
|
ConfigKey string
|
||||||
|
UID, GID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type envoyServiceConfig struct {
|
type envoyServiceConfig struct {
|
||||||
|
@ -176,12 +142,14 @@ var ServiceConfig = struct {
|
||||||
Postgrest serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]
|
Postgrest serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]
|
||||||
Auth serviceConfig[authEnvKeys, authConfigDefaults]
|
Auth serviceConfig[authEnvKeys, authConfigDefaults]
|
||||||
PGMeta serviceConfig[pgMetaEnvKeys, pgMetaDefaults]
|
PGMeta serviceConfig[pgMetaEnvKeys, pgMetaDefaults]
|
||||||
|
Studio serviceConfig[studioEnvKeys, studioDefaults]
|
||||||
Envoy envoyServiceConfig
|
Envoy envoyServiceConfig
|
||||||
JWT jwtConfig
|
JWT jwtConfig
|
||||||
}{
|
}{
|
||||||
Postgrest: serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]{
|
Postgrest: serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]{
|
||||||
Name: "postgrest",
|
Name: "postgrest",
|
||||||
EnvKeys: postgrestEnvKeys{
|
EnvKeys: postgrestEnvKeys{
|
||||||
|
Host: fixedEnvOf("PGRST_SERVER_HOST", "*"),
|
||||||
DBUri: "PGRST_DB_URI",
|
DBUri: "PGRST_DB_URI",
|
||||||
Schemas: stringSliceEnv{key: "PGRST_DB_SCHEMAS", separator: ","},
|
Schemas: stringSliceEnv{key: "PGRST_DB_SCHEMAS", separator: ","},
|
||||||
AnonRole: "PGRST_DB_ANON_ROLE",
|
AnonRole: "PGRST_DB_ANON_ROLE",
|
||||||
|
@ -191,28 +159,34 @@ var ServiceConfig = struct {
|
||||||
AppSettingsJWTExpiry: "PGRST_APP_SETTINGS_JWT_EXP",
|
AppSettingsJWTExpiry: "PGRST_APP_SETTINGS_JWT_EXP",
|
||||||
AdminServerPort: "PGRST_ADMIN_SERVER_PORT",
|
AdminServerPort: "PGRST_ADMIN_SERVER_PORT",
|
||||||
ExtraSearchPath: stringSliceEnv{key: "PGRST_DB_EXTRA_SEARCH_PATH", separator: ","},
|
ExtraSearchPath: stringSliceEnv{key: "PGRST_DB_EXTRA_SEARCH_PATH", separator: ","},
|
||||||
|
MaxRows: "PGRST_DB_MAX_ROWS",
|
||||||
|
OpenAPIProxyURI: "PGRST_OPENAPI_SERVER_PROXY_URI",
|
||||||
},
|
},
|
||||||
Defaults: postgrestConfigDefaults{
|
Defaults: postgrestConfigDefaults{
|
||||||
AnonRole: "anon",
|
AnonRole: "anon",
|
||||||
Schemas: []string{"public", "graphql_public"},
|
Schemas: []string{"public", "graphql_public"},
|
||||||
ExtraSearchPath: []string{"public", "extensions"},
|
ExtraSearchPath: []string{"public", "extensions"},
|
||||||
|
UID: 1000,
|
||||||
|
GID: 1000,
|
||||||
|
ServerPort: 3000,
|
||||||
|
AdminPort: 3001,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Auth: serviceConfig[authEnvKeys, authConfigDefaults]{
|
Auth: serviceConfig[authEnvKeys, authConfigDefaults]{
|
||||||
Name: "auth",
|
Name: "auth",
|
||||||
EnvKeys: authEnvKeys{
|
EnvKeys: authEnvKeys{
|
||||||
ApiHost: "GOTRUE_API_HOST",
|
ApiHost: fixedEnvOf("GOTRUE_API_HOST", "0.0.0.0"),
|
||||||
ApiPort: "GOTRUE_API_PORT",
|
ApiPort: fixedEnvOf("GOTRUE_API_PORT", "9999"),
|
||||||
ApiExternalUrl: "API_EXTERNAL_URL",
|
ApiExternalUrl: "API_EXTERNAL_URL",
|
||||||
DBDriver: "GOTRUE_DB_DRIVER",
|
DBDriver: fixedEnvOf("GOTRUE_DB_DRIVER", "postgres"),
|
||||||
DatabaseUrl: "GOTRUE_DB_DATABASE_URL",
|
DatabaseUrl: "GOTRUE_DB_DATABASE_URL",
|
||||||
SiteUrl: "GOTRUE_SITE_URL",
|
SiteUrl: "GOTRUE_SITE_URL",
|
||||||
AdditionalRedirectURLs: stringSliceEnv{key: "GOTRUE_URI_ALLOW_LIST", separator: ","},
|
AdditionalRedirectURLs: stringSliceEnv{key: "GOTRUE_URI_ALLOW_LIST", separator: ","},
|
||||||
DisableSignup: "GOTRUE_DISABLE_SIGNUP",
|
DisableSignup: "GOTRUE_DISABLE_SIGNUP",
|
||||||
JWTIssuer: "GOTRUE_JWT_ISSUER",
|
JWTIssuer: fixedEnvOf("GOTRUE_JWT_ISSUER", "supabase"),
|
||||||
JWTAdminRoles: "GOTRUE_JWT_ADMIN_ROLES",
|
JWTAdminRoles: fixedEnvOf("GOTRUE_JWT_ADMIN_ROLES", "service_role"),
|
||||||
JWTAudience: "GOTRUE_JWT_AUD",
|
JWTAudience: fixedEnvOf("GOTRUE_JWT_AUD", "authenticated"),
|
||||||
JwtDefaultGroup: "GOTRUE_JWT_DEFAULT_GROUP_NAME",
|
JwtDefaultGroup: fixedEnvOf("GOTRUE_JWT_DEFAULT_GROUP_NAME", "authenticated"),
|
||||||
JwtExpiry: "GOTRUE_JWT_EXP",
|
JwtExpiry: "GOTRUE_JWT_EXP",
|
||||||
JwtSecret: "GOTRUE_JWT_SECRET",
|
JwtSecret: "GOTRUE_JWT_SECRET",
|
||||||
EmailSignupDisabled: "GOTRUE_EXTERNAL_EMAIL_ENABLED",
|
EmailSignupDisabled: "GOTRUE_EXTERNAL_EMAIL_ENABLED",
|
||||||
|
@ -223,17 +197,13 @@ var ServiceConfig = struct {
|
||||||
AnonymousUsersEnabled: "GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED",
|
AnonymousUsersEnabled: "GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED",
|
||||||
},
|
},
|
||||||
Defaults: authConfigDefaults{
|
Defaults: authConfigDefaults{
|
||||||
ApiHost: "0.0.0.0",
|
|
||||||
ApiPort: 9999,
|
|
||||||
DbDriver: "postgres",
|
|
||||||
JwtIssuer: "supabase",
|
|
||||||
JwtAdminRoles: "service_role",
|
|
||||||
JwtAudience: "authenticated",
|
|
||||||
JwtDefaultGroupName: "authenticated",
|
|
||||||
MailerUrlPathsInvite: "/auth/v1/verify",
|
MailerUrlPathsInvite: "/auth/v1/verify",
|
||||||
MailerUrlPathsConfirmation: "/auth/v1/verify",
|
MailerUrlPathsConfirmation: "/auth/v1/verify",
|
||||||
MailerUrlPathsRecovery: "/auth/v1/verify",
|
MailerUrlPathsRecovery: "/auth/v1/verify",
|
||||||
MailerUrlPathsEmailChange: "/auth/v1/verify",
|
MailerUrlPathsEmailChange: "/auth/v1/verify",
|
||||||
|
APIPort: 9999,
|
||||||
|
UID: 1000,
|
||||||
|
GID: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PGMeta: serviceConfig[pgMetaEnvKeys, pgMetaDefaults]{
|
PGMeta: serviceConfig[pgMetaEnvKeys, pgMetaDefaults]{
|
||||||
|
@ -249,11 +219,33 @@ var ServiceConfig = struct {
|
||||||
Defaults: pgMetaDefaults{
|
Defaults: pgMetaDefaults{
|
||||||
APIPort: 8080,
|
APIPort: 8080,
|
||||||
DBPort: "5432",
|
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{
|
Envoy: envoyServiceConfig{
|
||||||
Defaults: envoyDefaults{
|
Defaults: envoyDefaults{
|
||||||
"config.yaml",
|
ConfigKey: "config.yaml",
|
||||||
|
UID: 65532,
|
||||||
|
GID: 65532,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
JWT: jwtConfig{
|
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")
|
corelog.Info("Defaulting JWT")
|
||||||
|
|
||||||
if core.Spec.JWT == nil {
|
if core.Spec.JWT == nil {
|
||||||
core.Spec.JWT = new(supabasev1alpha1.JwtSpec)
|
core.Spec.JWT = new(supabasev1alpha1.CoreJwtSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
if core.Spec.JWT.SecretRef == nil {
|
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()}).
|
WithDefaulter(&CoreCustomDefaulter{Client: mgr.GetClient()}).
|
||||||
Complete()
|
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"})
|
err = SetupAPIGatewayWebhookWithManager(mgr, WebhookConfig{CurrentNamespace: "default"})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = SetupDashboardWebhookWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
// +kubebuilder:scaffold:webhook
|
// +kubebuilder:scaffold:webhook
|
||||||
|
|
||||||
go func() {
|
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