diff --git a/app/cli/cmd/casbackend.go b/app/cli/cmd/casbackend.go index 250d78214..1dee5ee73 100644 --- a/app/cli/cmd/casbackend.go +++ b/app/cli/cmd/casbackend.go @@ -25,6 +25,7 @@ import ( var ( isDefaultCASBackendUpdateOption *bool + isFallbackCASBackendUpdateOption *bool descriptionCASBackendUpdateOption *string maxBytesCASBackendOption string parsedMaxBytes *int64 @@ -47,6 +48,7 @@ func newCASBackendAddCmd() *cobra.Command { } cmd.PersistentFlags().Bool("default", false, "set the backend as default in your organization") + cmd.PersistentFlags().Bool("fallback", false, "set the backend as fallback in your organization") cmd.PersistentFlags().String("description", "", "descriptive information for this registration") cmd.PersistentFlags().String("name", "", "CAS backend name") cmd.PersistentFlags().StringVar(&maxBytesCASBackendOption, "max-bytes", "", "Maximum size for each blob stored in this backend (e.g., 100MB, 1GB)") @@ -60,10 +62,11 @@ func newCASBackendAddCmd() *cobra.Command { func newCASBackendUpdateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "update", - Short: "Update a CAS backend description, credentials, default status, or max bytes", + Short: "Update a CAS backend description, credentials, default status, fallback status, or max bytes", } cmd.PersistentFlags().Bool("default", false, "set the backend as default in your organization") + cmd.PersistentFlags().Bool("fallback", false, "set the backend as fallback in your organization") cmd.PersistentFlags().String("description", "", "descriptive information for this registration") cmd.PersistentFlags().String("name", "", "CAS backend name") cmd.PersistentFlags().StringVar(&maxBytesCASBackendOption, "max-bytes", "", "Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends.") @@ -148,7 +151,7 @@ func parseMaxBytesOption() error { return nil } -// captureUpdateFlags reads the --default and --description flags only when explicitly set and +// captureUpdateFlags reads the --default, --fallback, and --description flags only when explicitly set and // stores their values in the package-level pointer options. This avoids treating their zero // values as an intention to update. func captureUpdateFlags(cmd *cobra.Command) error { @@ -160,6 +163,14 @@ func captureUpdateFlags(cmd *cobra.Command) error { isDefaultCASBackendUpdateOption = &v } + if f := cmd.Flags().Lookup("fallback"); f != nil && f.Changed { + v, err := cmd.Flags().GetBool("fallback") + if err != nil { + return err + } + isFallbackCASBackendUpdateOption = &v + } + if f := cmd.Flags().Lookup("description"); f != nil && f.Changed { v, err := cmd.Flags().GetString("description") if err != nil { diff --git a/app/cli/cmd/casbackend_add_azureblob.go b/app/cli/cmd/casbackend_add_azureblob.go index 4f3be9639..8812de496 100644 --- a/app/cli/cmd/casbackend_add_azureblob.go +++ b/app/cli/cmd/casbackend_add_azureblob.go @@ -39,6 +39,9 @@ func newCASBackendAddAzureBlobStorageCmd() *cobra.Command { isDefault, err := cmd.Flags().GetBool("default") cobra.CheckErr(err) + isFallback, err := cmd.Flags().GetBool("fallback") + cobra.CheckErr(err) + name, err := cmd.Flags().GetString("name") cobra.CheckErr(err) @@ -70,6 +73,7 @@ func newCASBackendAddAzureBlobStorageCmd() *cobra.Command { "clientSecret": clientSecret, }, Default: isDefault, + Fallback: isFallback, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/cmd/casbackend_add_oci.go b/app/cli/cmd/casbackend_add_oci.go index 16ab69c5f..e8a416f78 100644 --- a/app/cli/cmd/casbackend_add_oci.go +++ b/app/cli/cmd/casbackend_add_oci.go @@ -36,6 +36,9 @@ func newCASBackendAddOCICmd() *cobra.Command { isDefault, err := cmd.Flags().GetBool("default") cobra.CheckErr(err) + isFallback, err := cmd.Flags().GetBool("fallback") + cobra.CheckErr(err) + name, err := cmd.Flags().GetString("name") cobra.CheckErr(err) @@ -60,6 +63,7 @@ func newCASBackendAddOCICmd() *cobra.Command { "password": password, }, Default: isDefault, + Fallback: isFallback, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/cmd/casbackend_add_s3.go b/app/cli/cmd/casbackend_add_s3.go index f01389298..7c8ca9993 100644 --- a/app/cli/cmd/casbackend_add_s3.go +++ b/app/cli/cmd/casbackend_add_s3.go @@ -37,6 +37,9 @@ func newCASBackendAddAWSS3Cmd() *cobra.Command { isDefault, err := cmd.Flags().GetBool("default") cobra.CheckErr(err) + isFallback, err := cmd.Flags().GetBool("fallback") + cobra.CheckErr(err) + name, err := cmd.Flags().GetString("name") cobra.CheckErr(err) @@ -69,6 +72,7 @@ func newCASBackendAddAWSS3Cmd() *cobra.Command { "region": region, }, Default: isDefault, + Fallback: isFallback, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/cmd/casbackend_list.go b/app/cli/cmd/casbackend_list.go index 63f5bcc23..d25214ef6 100644 --- a/app/cli/cmd/casbackend_list.go +++ b/app/cli/cmd/casbackend_list.go @@ -58,7 +58,7 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error { } t := output.NewTableWriter() - header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default", "Status"} + header := table.Row{"Name", "Location", "Provider", "Description", "Limits", "Default", "Fallback", "Status"} if full { header = append(header, "Created At", "Validated At") } @@ -75,7 +75,7 @@ func casBackendListTableOutput(backends []*action.CASBackendItem) error { validationStatus = strings.Join([]string{validationStatus, wrap.String(*b.ValidationError, 50)}, "\n") } - r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default, validationStatus} + r := table.Row{b.Name, wrap.String(b.Location, 35), b.Provider, wrap.String(b.Description, 35), limits, b.Default, b.Fallback, validationStatus} if full { r = append(r, b.CreatedAt.Format(time.RFC822), b.ValidatedAt.Format(time.RFC822)) } diff --git a/app/cli/cmd/casbackend_update_azureblob.go b/app/cli/cmd/casbackend_update_azureblob.go index 1886c7583..1025c2ca7 100644 --- a/app/cli/cmd/casbackend_update_azureblob.go +++ b/app/cli/cmd/casbackend_update_azureblob.go @@ -53,6 +53,7 @@ func newCASBackendUpdateAzureBlobCmd() *cobra.Command { "clientSecret": clientSecret, }, Default: isDefaultCASBackendUpdateOption, + Fallback: isFallbackCASBackendUpdateOption, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/cmd/casbackend_update_inline.go b/app/cli/cmd/casbackend_update_inline.go index a2d38f50f..5d6d6e365 100644 --- a/app/cli/cmd/casbackend_update_inline.go +++ b/app/cli/cmd/casbackend_update_inline.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ func newCASBackendUpdateInlineCmd() *cobra.Command { var backendName string cmd := &cobra.Command{ Use: "inline", - Short: "Update the Inline, fallback CAS Backend description or default status", + Short: "Update the Inline CAS Backend description, default status, or fallback status", RunE: func(cmd *cobra.Command, args []string) error { // capture flags only when explicitly set if err := captureUpdateFlags(cmd); err != nil { @@ -45,6 +45,7 @@ func newCASBackendUpdateInlineCmd() *cobra.Command { Name: backendName, Description: descriptionCASBackendUpdateOption, Default: isDefaultCASBackendUpdateOption, + Fallback: isFallbackCASBackendUpdateOption, } res, err := action.NewCASBackendUpdate(ActionOpts).Run(opts) diff --git a/app/cli/cmd/casbackend_update_oci.go b/app/cli/cmd/casbackend_update_oci.go index 7b70091f3..9340279a4 100644 --- a/app/cli/cmd/casbackend_update_oci.go +++ b/app/cli/cmd/casbackend_update_oci.go @@ -52,6 +52,7 @@ func newCASBackendUpdateOCICmd() *cobra.Command { "password": password, }, Default: isDefaultCASBackendUpdateOption, + Fallback: isFallbackCASBackendUpdateOption, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/cmd/casbackend_update_s3.go b/app/cli/cmd/casbackend_update_s3.go index 9b0c792c8..23529172b 100644 --- a/app/cli/cmd/casbackend_update_s3.go +++ b/app/cli/cmd/casbackend_update_s3.go @@ -53,6 +53,7 @@ func newCASBackendUpdateAWSS3Cmd() *cobra.Command { "region": region, }, Default: isDefaultCASBackendUpdateOption, + Fallback: isFallbackCASBackendUpdateOption, MaxBytes: parsedMaxBytes, } diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 21c14c386..1066b34c7 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -640,6 +640,7 @@ Options ``` --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -h, --help help for add --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB) --name string CAS backend name @@ -691,6 +692,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB) --name string CAS backend name @@ -731,6 +733,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB) --name string CAS backend name @@ -770,6 +773,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB) --name string CAS backend name @@ -807,6 +811,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB) --name string CAS backend name @@ -915,13 +920,14 @@ Options inherited from parent commands ### chainloop cas-backend update -Update a CAS backend description, credentials, default status, or max bytes +Update a CAS backend description, credentials, default status, fallback status, or max bytes Options ``` --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -h, --help help for update --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. --name string CAS backend name @@ -972,6 +978,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. -n, --org string organization name @@ -1009,6 +1016,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. -n, --org string organization name @@ -1047,6 +1055,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. --name string CAS backend name @@ -1058,7 +1067,7 @@ Options inherited from parent commands #### chainloop cas-backend update inline -Update the Inline, fallback CAS Backend description or default status +Update the Inline CAS Backend description, default status, or fallback status ``` chainloop cas-backend update inline [flags] @@ -1082,6 +1091,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. -n, --org string organization name @@ -1118,6 +1128,7 @@ Options inherited from parent commands --debug Enable debug/verbose logging mode --default set the backend as default in your organization --description string descriptive information for this registration +--fallback set the backend as fallback in your organization -i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) --max-bytes string Maximum size for each blob stored in this backend (e.g., 100MB, 1GB). Note: not supported for inline backends. -n, --org string organization name diff --git a/app/cli/pkg/action/casbackend_add.go b/app/cli/pkg/action/casbackend_add.go index 801bef23b..8ae653b88 100644 --- a/app/cli/pkg/action/casbackend_add.go +++ b/app/cli/pkg/action/casbackend_add.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ type NewCASBackendAddOpts struct { Provider string Description string Default bool + Fallback bool Credentials map[string]any MaxBytes *int64 } @@ -55,6 +56,7 @@ func (action *CASBackendAdd) Run(opts *NewCASBackendAddOpts) (*CASBackendItem, e Provider: opts.Provider, Description: opts.Description, Default: opts.Default, + Fallback: opts.Fallback, Credentials: credentials, MaxBytes: opts.MaxBytes, }) diff --git a/app/cli/pkg/action/casbackend_list.go b/app/cli/pkg/action/casbackend_list.go index 85121f48f..b51eb4698 100644 --- a/app/cli/pkg/action/casbackend_list.go +++ b/app/cli/pkg/action/casbackend_list.go @@ -33,6 +33,7 @@ type CASBackendItem struct { Description string `json:"description"` Provider string `json:"provider"` Default bool `json:"default"` + Fallback bool `json:"fallback"` Inline bool `json:"inline"` Limits *CASBackendLimits `json:"limits"` ValidationStatus ValidationStatus `json:"validationStatus"` @@ -85,6 +86,7 @@ func pbCASBackendItemToAction(in *pb.CASBackendItem) *CASBackendItem { Description: in.Description, Provider: in.Provider, Default: in.Default, + Fallback: in.Fallback, CreatedAt: toTimePtr(in.CreatedAt.AsTime()), ValidatedAt: toTimePtr(in.ValidatedAt.AsTime()), Inline: in.IsInline, diff --git a/app/cli/pkg/action/casbackend_update.go b/app/cli/pkg/action/casbackend_update.go index 0cb577f65..1b2a06bf1 100644 --- a/app/cli/pkg/action/casbackend_update.go +++ b/app/cli/pkg/action/casbackend_update.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ type NewCASBackendUpdateOpts struct { Name string Description *string Default *bool + Fallback *bool Credentials map[string]any MaxBytes *int64 } @@ -55,6 +56,7 @@ func (action *CASBackendUpdate) Run(opts *NewCASBackendUpdateOpts) (*CASBackendI Name: opts.Name, Description: opts.Description, Default: opts.Default, + Fallback: opts.Fallback, Credentials: credentials, MaxBytes: opts.MaxBytes, }) diff --git a/app/controlplane/api/controlplane/v1/cas_backends.pb.go b/app/controlplane/api/controlplane/v1/cas_backends.pb.go index 569e7258d..8f89e1981 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends.pb.go +++ b/app/controlplane/api/controlplane/v1/cas_backends.pb.go @@ -180,6 +180,8 @@ type CASBackendServiceCreateRequest struct { Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` // Set as default in your organization Default bool `protobuf:"varint,4,opt,name=default,proto3" json:"default,omitempty"` + // Set as fallback in your organization + Fallback bool `protobuf:"varint,8,opt,name=fallback,proto3" json:"fallback,omitempty"` // Arbitrary configuration for the integration Credentials *structpb.Struct `protobuf:"bytes,5,opt,name=credentials,proto3" json:"credentials,omitempty"` Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` @@ -248,6 +250,13 @@ func (x *CASBackendServiceCreateRequest) GetDefault() bool { return false } +func (x *CASBackendServiceCreateRequest) GetFallback() bool { + if x != nil { + return x.Fallback + } + return false +} + func (x *CASBackendServiceCreateRequest) GetCredentials() *structpb.Struct { if x != nil { return x.Credentials @@ -325,6 +334,8 @@ type CASBackendServiceUpdateRequest struct { Description *string `protobuf:"bytes,2,opt,name=description,proto3,oneof" json:"description,omitempty"` // Set as default in your organization Default *bool `protobuf:"varint,3,opt,name=default,proto3,oneof" json:"default,omitempty"` + // Set as fallback in your organization + Fallback *bool `protobuf:"varint,6,opt,name=fallback,proto3,oneof" json:"fallback,omitempty"` // Credentials, useful for rotation Credentials *structpb.Struct `protobuf:"bytes,4,opt,name=credentials,proto3" json:"credentials,omitempty"` // Maximum size in bytes for each blob stored in this backend. @@ -384,6 +395,13 @@ func (x *CASBackendServiceUpdateRequest) GetDefault() bool { return false } +func (x *CASBackendServiceUpdateRequest) GetFallback() bool { + if x != nil && x.Fallback != nil { + return *x.Fallback + } + return false +} + func (x *CASBackendServiceUpdateRequest) GetCredentials() *structpb.Struct { if x != nil { return x.Credentials @@ -609,29 +627,32 @@ const file_controlplane_v1_cas_backends_proto_rawDesc = "" + "\"controlplane/v1/cas_backends.proto\x12\x0fcontrolplane.v1\x1a\x1bbuf/validate/validate.proto\x1a'controlplane/v1/response_messages.proto\x1a\x13errors/errors.proto\x1a\x1cgoogle/protobuf/struct.proto\"\x1e\n" + "\x1cCASBackendServiceListRequest\"X\n" + "\x1dCASBackendServiceListResponse\x127\n" + - "\x06result\x18\x01 \x03(\v2\x1f.controlplane.v1.CASBackendItemR\x06result\"\xb6\x02\n" + + "\x06result\x18\x01 \x03(\v2\x1f.controlplane.v1.CASBackendItemR\x06result\"\xd2\x02\n" + "\x1eCASBackendServiceCreateRequest\x12#\n" + "\blocation\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\blocation\x12#\n" + "\bprovider\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\bprovider\x12 \n" + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x18\n" + - "\adefault\x18\x04 \x01(\bR\adefault\x12A\n" + + "\adefault\x18\x04 \x01(\bR\adefault\x12\x1a\n" + + "\bfallback\x18\b \x01(\bR\bfallback\x12A\n" + "\vcredentials\x18\x05 \x01(\v2\x17.google.protobuf.StructB\x06\xbaH\x03\xc8\x01\x01R\vcredentials\x12\x1b\n" + "\x04name\x18\x06 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04name\x12 \n" + "\tmax_bytes\x18\a \x01(\x03H\x00R\bmaxBytes\x88\x01\x01B\f\n" + "\n" + "_max_bytes\"Z\n" + "\x1fCASBackendServiceCreateResponse\x127\n" + - "\x06result\x18\x01 \x01(\v2\x1f.controlplane.v1.CASBackendItemR\x06result\"\x87\x03\n" + + "\x06result\x18\x01 \x01(\v2\x1f.controlplane.v1.CASBackendItemR\x06result\"\xb5\x03\n" + "\x1eCASBackendServiceUpdateRequest\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\x12%\n" + "\vdescription\x18\x02 \x01(\tH\x00R\vdescription\x88\x01\x01\x12\x1d\n" + - "\adefault\x18\x03 \x01(\bH\x01R\adefault\x88\x01\x01\x129\n" + + "\adefault\x18\x03 \x01(\bH\x01R\adefault\x88\x01\x01\x12\x1f\n" + + "\bfallback\x18\x06 \x01(\bH\x02R\bfallback\x88\x01\x01\x129\n" + "\vcredentials\x18\x04 \x01(\v2\x17.google.protobuf.StructR\vcredentials\x12 \n" + - "\tmax_bytes\x18\x05 \x01(\x03H\x02R\bmaxBytes\x88\x01\x01B\x0e\n" + + "\tmax_bytes\x18\x05 \x01(\x03H\x03R\bmaxBytes\x88\x01\x01B\x0e\n" + "\f_descriptionB\n" + "\n" + - "\b_defaultB\f\n" + + "\b_defaultB\v\n" + + "\t_fallbackB\f\n" + "\n" + "_max_bytes\"Z\n" + "\x1fCASBackendServiceUpdateResponse\x127\n" + diff --git a/app/controlplane/api/controlplane/v1/cas_backends.proto b/app/controlplane/api/controlplane/v1/cas_backends.proto index df6b7d05c..b005ad419 100644 --- a/app/controlplane/api/controlplane/v1/cas_backends.proto +++ b/app/controlplane/api/controlplane/v1/cas_backends.proto @@ -47,6 +47,8 @@ message CASBackendServiceCreateRequest { string description = 3; // Set as default in your organization bool default = 4; + // Set as fallback in your organization + bool fallback = 8; // Arbitrary configuration for the integration google.protobuf.Struct credentials = 5 [(buf.validate.field).required = true]; string name = 6 [(buf.validate.field).string.min_len = 1]; @@ -77,6 +79,8 @@ message CASBackendServiceUpdateRequest { optional string description = 2; // Set as default in your organization optional bool default = 3; + // Set as fallback in your organization + optional bool fallback = 6; // Credentials, useful for rotation google.protobuf.Struct credentials = 4; // Maximum size in bytes for each blob stored in this backend. diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index 26c2cd420..9fa6bed61 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -2000,8 +2000,10 @@ type CASBackendItem struct { // Error message if validation failed ValidationError *string `protobuf:"bytes,12,opt,name=validation_error,json=validationError,proto3,oneof" json:"validation_error,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Wether it's the fallback backend in the organization + Fallback bool `protobuf:"varint,14,opt,name=fallback,proto3" json:"fallback,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CASBackendItem) Reset() { @@ -2125,6 +2127,13 @@ func (x *CASBackendItem) GetUpdatedAt() *timestamppb.Timestamp { return nil } +func (x *CASBackendItem) GetFallback() bool { + if x != nil { + return x.Fallback + } + return false +} + type APITokenItem struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -2777,7 +2786,7 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\x1fPolicyViolationBlockingStrategy\x122\n" + ".POLICY_VIOLATION_BLOCKING_STRATEGY_UNSPECIFIED\x10\x00\x12,\n" + "(POLICY_VIOLATION_BLOCKING_STRATEGY_BLOCK\x10\x01\x12/\n" + - "+POLICY_VIOLATION_BLOCKING_STRATEGY_ADVISORY\x10\x02\"\xf5\x05\n" + + "+POLICY_VIOLATION_BLOCKING_STRATEGY_ADVISORY\x10\x02\"\x91\x06\n" + "\x0eCASBackendItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\v \x01(\tR\x04name\x12\x1a\n" + @@ -2794,7 +2803,8 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + " \x01(\bR\bisInline\x12.\n" + "\x10validation_error\x18\f \x01(\tH\x00R\x0fvalidationError\x88\x01\x01\x129\n" + "\n" + - "updated_at\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x1a%\n" + + "updated_at\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x1a\n" + + "\bfallback\x18\x0e \x01(\bR\bfallback\x1a%\n" + "\x06Limits\x12\x1b\n" + "\tmax_bytes\x18\x01 \x01(\x03R\bmaxBytes\"n\n" + "\x10ValidationStatus\x12!\n" + diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index cb12e2ce8..89da57381 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -312,6 +312,8 @@ message CASBackendItem { // Error message if validation failed optional string validation_error = 12; google.protobuf.Timestamp updated_at = 13; + // Wether it's the fallback backend in the organization + bool fallback = 14; message Limits { // Max number of bytes allowed to be stored in this backend diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts b/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts index 9e1350e71..358e56d65 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/cas_backends.ts @@ -67,6 +67,8 @@ export interface CASBackendServiceCreateRequest { description: string; /** Set as default in your organization */ default: boolean; + /** Set as fallback in your organization */ + fallback: boolean; /** Arbitrary configuration for the integration */ credentials?: { [key: string]: any }; name: string; @@ -98,6 +100,10 @@ export interface CASBackendServiceUpdateRequest { default?: | boolean | undefined; + /** Set as fallback in your organization */ + fallback?: + | boolean + | undefined; /** Credentials, useful for rotation */ credentials?: { [key: string]: any }; /** Maximum size in bytes for each blob stored in this backend. */ @@ -234,6 +240,7 @@ function createBaseCASBackendServiceCreateRequest(): CASBackendServiceCreateRequ provider: "", description: "", default: false, + fallback: false, credentials: undefined, name: "", maxBytes: undefined, @@ -254,6 +261,9 @@ export const CASBackendServiceCreateRequest = { if (message.default === true) { writer.uint32(32).bool(message.default); } + if (message.fallback === true) { + writer.uint32(64).bool(message.fallback); + } if (message.credentials !== undefined) { Struct.encode(Struct.wrap(message.credentials), writer.uint32(42).fork()).ldelim(); } @@ -301,6 +311,13 @@ export const CASBackendServiceCreateRequest = { message.default = reader.bool(); continue; + case 8: + if (tag !== 64) { + break; + } + + message.fallback = reader.bool(); + continue; case 5: if (tag !== 42) { break; @@ -337,6 +354,7 @@ export const CASBackendServiceCreateRequest = { provider: isSet(object.provider) ? String(object.provider) : "", description: isSet(object.description) ? String(object.description) : "", default: isSet(object.default) ? Boolean(object.default) : false, + fallback: isSet(object.fallback) ? Boolean(object.fallback) : false, credentials: isObject(object.credentials) ? object.credentials : undefined, name: isSet(object.name) ? String(object.name) : "", maxBytes: isSet(object.maxBytes) ? Number(object.maxBytes) : undefined, @@ -349,6 +367,7 @@ export const CASBackendServiceCreateRequest = { message.provider !== undefined && (obj.provider = message.provider); message.description !== undefined && (obj.description = message.description); message.default !== undefined && (obj.default = message.default); + message.fallback !== undefined && (obj.fallback = message.fallback); message.credentials !== undefined && (obj.credentials = message.credentials); message.name !== undefined && (obj.name = message.name); message.maxBytes !== undefined && (obj.maxBytes = Math.round(message.maxBytes)); @@ -367,6 +386,7 @@ export const CASBackendServiceCreateRequest = { message.provider = object.provider ?? ""; message.description = object.description ?? ""; message.default = object.default ?? false; + message.fallback = object.fallback ?? false; message.credentials = object.credentials ?? undefined; message.name = object.name ?? ""; message.maxBytes = object.maxBytes ?? undefined; @@ -435,7 +455,14 @@ export const CASBackendServiceCreateResponse = { }; function createBaseCASBackendServiceUpdateRequest(): CASBackendServiceUpdateRequest { - return { name: "", description: undefined, default: undefined, credentials: undefined, maxBytes: undefined }; + return { + name: "", + description: undefined, + default: undefined, + fallback: undefined, + credentials: undefined, + maxBytes: undefined, + }; } export const CASBackendServiceUpdateRequest = { @@ -449,6 +476,9 @@ export const CASBackendServiceUpdateRequest = { if (message.default !== undefined) { writer.uint32(24).bool(message.default); } + if (message.fallback !== undefined) { + writer.uint32(48).bool(message.fallback); + } if (message.credentials !== undefined) { Struct.encode(Struct.wrap(message.credentials), writer.uint32(34).fork()).ldelim(); } @@ -486,6 +516,13 @@ export const CASBackendServiceUpdateRequest = { message.default = reader.bool(); continue; + case 6: + if (tag !== 48) { + break; + } + + message.fallback = reader.bool(); + continue; case 4: if (tag !== 34) { break; @@ -514,6 +551,7 @@ export const CASBackendServiceUpdateRequest = { name: isSet(object.name) ? String(object.name) : "", description: isSet(object.description) ? String(object.description) : undefined, default: isSet(object.default) ? Boolean(object.default) : undefined, + fallback: isSet(object.fallback) ? Boolean(object.fallback) : undefined, credentials: isObject(object.credentials) ? object.credentials : undefined, maxBytes: isSet(object.maxBytes) ? Number(object.maxBytes) : undefined, }; @@ -524,6 +562,7 @@ export const CASBackendServiceUpdateRequest = { message.name !== undefined && (obj.name = message.name); message.description !== undefined && (obj.description = message.description); message.default !== undefined && (obj.default = message.default); + message.fallback !== undefined && (obj.fallback = message.fallback); message.credentials !== undefined && (obj.credentials = message.credentials); message.maxBytes !== undefined && (obj.maxBytes = Math.round(message.maxBytes)); return obj; @@ -540,6 +579,7 @@ export const CASBackendServiceUpdateRequest = { message.name = object.name ?? ""; message.description = object.description ?? undefined; message.default = object.default ?? undefined; + message.fallback = object.fallback ?? undefined; message.credentials = object.credentials ?? undefined; message.maxBytes = object.maxBytes ?? undefined; return message; diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index 7fe2b2932..bc696dfb6 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -674,6 +674,8 @@ export interface CASBackendItem { /** Error message if validation failed */ validationError?: string | undefined; updatedAt?: Date; + /** Wether it's the fallback backend in the organization */ + fallback: boolean; } export enum CASBackendItem_ValidationStatus { @@ -3939,6 +3941,7 @@ function createBaseCASBackendItem(): CASBackendItem { isInline: false, validationError: undefined, updatedAt: undefined, + fallback: false, }; } @@ -3983,6 +3986,9 @@ export const CASBackendItem = { if (message.updatedAt !== undefined) { Timestamp.encode(toTimestamp(message.updatedAt), writer.uint32(106).fork()).ldelim(); } + if (message.fallback === true) { + writer.uint32(112).bool(message.fallback); + } return writer; }, @@ -4084,6 +4090,13 @@ export const CASBackendItem = { message.updatedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); continue; + case 14: + if (tag !== 112) { + break; + } + + message.fallback = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -4110,6 +4123,7 @@ export const CASBackendItem = { isInline: isSet(object.isInline) ? Boolean(object.isInline) : false, validationError: isSet(object.validationError) ? String(object.validationError) : undefined, updatedAt: isSet(object.updatedAt) ? fromJsonTimestamp(object.updatedAt) : undefined, + fallback: isSet(object.fallback) ? Boolean(object.fallback) : false, }; }, @@ -4130,6 +4144,7 @@ export const CASBackendItem = { message.isInline !== undefined && (obj.isInline = message.isInline); message.validationError !== undefined && (obj.validationError = message.validationError); message.updatedAt !== undefined && (obj.updatedAt = message.updatedAt.toISOString()); + message.fallback !== undefined && (obj.fallback = message.fallback); return obj; }, @@ -4154,6 +4169,7 @@ export const CASBackendItem = { message.isInline = object.isInline ?? false; message.validationError = object.validationError ?? undefined; message.updatedAt = object.updatedAt ?? undefined; + message.fallback = object.fallback ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.jsonschema.json index 48ed1c727..84c6561c5 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.jsonschema.json @@ -50,6 +50,10 @@ "description": { "type": "string" }, + "fallback": { + "description": "Wether it's the fallback backend in the organization", + "type": "boolean" + }, "id": { "type": "string" }, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.schema.json index 159678404..9860a549d 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendItem.schema.json @@ -50,6 +50,10 @@ "description": { "type": "string" }, + "fallback": { + "description": "Wether it's the fallback backend in the organization", + "type": "boolean" + }, "id": { "type": "string" }, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.jsonschema.json index 1e7d16b3a..c1ca0c709 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.jsonschema.json @@ -31,6 +31,10 @@ "description": "Descriptive name", "type": "string" }, + "fallback": { + "description": "Set as fallback in your organization", + "type": "boolean" + }, "location": { "description": "Location, e.g. bucket name, OCI bucket name, ...", "minLength": 1, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.schema.json index 29073b13a..d61a8f4f7 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceCreateRequest.schema.json @@ -31,6 +31,10 @@ "description": "Descriptive name", "type": "string" }, + "fallback": { + "description": "Set as fallback in your organization", + "type": "boolean" + }, "location": { "description": "Location, e.g. bucket name, OCI bucket name, ...", "minLength": 1, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.jsonschema.json index 740b8c1ce..2032ef15e 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.jsonschema.json @@ -32,6 +32,10 @@ "description": "Description", "type": "string" }, + "fallback": { + "description": "Set as fallback in your organization", + "type": "boolean" + }, "maxBytes": { "anyOf": [ { diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.schema.json index 19db73ddd..3cfaf2187 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.CASBackendServiceUpdateRequest.schema.json @@ -32,6 +32,10 @@ "description": "Description", "type": "string" }, + "fallback": { + "description": "Set as fallback in your organization", + "type": "boolean" + }, "max_bytes": { "anyOf": [ { diff --git a/app/controlplane/cmd/main.go b/app/controlplane/cmd/main.go index a4423e0d7..4713761af 100644 --- a/app/controlplane/cmd/main.go +++ b/app/controlplane/cmd/main.go @@ -171,7 +171,7 @@ func main() { go app.casBackendChecker.Start(ctx, &biz.CASBackendCheckerOpts{ CheckInterval: 30 * time.Minute, InitialDelay: initialDelay, - OnlyDefaults: toPtr(true), + OnlyDefaultsOrFallbacks: toPtr(true), }) // Start the background CAS Backend checker for ALL backends (every 24 hours) @@ -179,7 +179,7 @@ func main() { go app.casBackendChecker.Start(ctx, &biz.CASBackendCheckerOpts{ CheckInterval: 24 * time.Hour, InitialDelay: (24 * time.Hour) + jitter, - OnlyDefaults: toPtr(false), + OnlyDefaultsOrFallbacks: toPtr(false), }) } diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index b78b6c8cd..60e338ed9 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -174,17 +174,15 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer return nil, errors.NotFound("not found", "contract not found") } - // find the default CAS backend to associate the workflow - backend, err := s.casUC.FindDefaultBackend(context.Background(), robotAccount.OrgID) - if err != nil && !biz.IsNotFound(err) { - return nil, fmt.Errorf("failed to find default CAS backend: %w", err) - } else if err != nil { - return nil, errors.NotFound("not found", "default CAS backend not found") - } - - // Check the status of the backend - if backend.ValidationStatus != biz.CASBackendValidationOK { - return nil, cpAPI.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached") + // Find the default or fallback CAS backend to associate the workflow + backend, err := s.casUC.FindDefaultOrFallbackBackend(context.Background(), robotAccount.OrgID) + if err != nil { + if biz.IsNotFound(err) { + return nil, errors.NotFound("not found", "default CAS backend not found") + } else if biz.IsErrValidation(err) { + return nil, err + } + return nil, fmt.Errorf("failed to find CAS backend: %w", err) } // Create workflowRun diff --git a/app/controlplane/internal/service/casbackend.go b/app/controlplane/internal/service/casbackend.go index 40f4df2a7..8ed129164 100644 --- a/app/controlplane/internal/service/casbackend.go +++ b/app/controlplane/internal/service/casbackend.go @@ -89,7 +89,7 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic } // For now we only support one backend which is set as default - res, err := s.uc.Create(ctx, currentOrg.ID, req.Name, req.Location, req.Description, biz.CASBackendProvider(req.Provider), creds, req.Default, maxBytes) + res, err := s.uc.Create(ctx, currentOrg.ID, req.Name, req.Location, req.Description, biz.CASBackendProvider(req.Provider), creds, req.Default, req.Fallback, maxBytes) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -136,7 +136,7 @@ func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServic } // For now we only support one backend which is set as default - res, err := s.uc.Update(ctx, currentOrg.ID, backend.ID.String(), req.Description, creds, req.Default, maxBytes) + res, err := s.uc.Update(ctx, currentOrg.ID, backend.ID.String(), req.Description, creds, req.Default, req.Fallback, maxBytes) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -201,6 +201,7 @@ func bizCASBackendToPb(in *biz.CASBackend) *pb.CASBackendItem { ValidatedAt: timestamppb.New(*in.ValidatedAt), Provider: string(in.Provider), Default: in.Default, + Fallback: in.Fallback, IsInline: in.Inline, } diff --git a/app/controlplane/internal/usercontext/orgrequirements_middleware.go b/app/controlplane/internal/usercontext/orgrequirements_middleware.go index 274050dc4..4db53a14c 100644 --- a/app/controlplane/internal/usercontext/orgrequirements_middleware.go +++ b/app/controlplane/internal/usercontext/orgrequirements_middleware.go @@ -62,7 +62,7 @@ func ValidateCASBackend(uc biz.CASBackendReader) middleware.Middleware { } } -// validateRepoIfNeeded will re-run a validation and return the updated repository +// validateCASBackend will re-run a validation and return the updated repository func validateCASBackend(ctx context.Context, uc biz.CASBackendReader, repo *biz.CASBackend) (*biz.CASBackend, error) { // re-run the validation if err := uc.PerformValidation(ctx, repo.ID.String()); err != nil { @@ -102,18 +102,17 @@ func BlockIfCASBackendNotValid(uc biz.CASBackendReader) middleware.Middleware { return nil, errors.New("organization not found") } - // 1 - Figure out main repository for this organization - repo, err := uc.FindDefaultBackend(ctx, org.ID) - if err != nil && !biz.IsNotFound(err) { + // Check if there's a valid CAS backend (default or fallback) + _, err := uc.FindDefaultOrFallbackBackend(ctx, org.ID) + if err != nil { + if biz.IsNotFound(err) { + return nil, v1.ErrorCasBackendErrorReasonRequired("your organization does not have a CAS Backend configured yet") + } else if biz.IsErrValidation(err) { + return nil, v1.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached") + } return nil, fmt.Errorf("checking for CAS backends in the org: %w", err) - } else if repo == nil { - return nil, v1.ErrorCasBackendErrorReasonRequired("your organization does not have a CAS Backend configured yet") } - // 2 - compare the status - if repo.ValidationStatus != biz.CASBackendValidationOK { - return nil, v1.ErrorCasBackendErrorReasonInvalid("your CAS backend can't be reached") - } return handler(ctx, req) } } diff --git a/app/controlplane/pkg/biz/casbackend.go b/app/controlplane/pkg/biz/casbackend.go index c8d1ce0f5..3b7289741 100644 --- a/app/controlplane/pkg/biz/casbackend.go +++ b/app/controlplane/pkg/biz/casbackend.go @@ -67,7 +67,7 @@ type CASBackend struct { Default bool // it's a inline backend, the artifacts are embedded in the attestation Inline bool - // It's a fallback backend, it cannot be deleted + // It's a fallback backend, used when the default backend is unreachable Fallback bool Limits *CASBackendLimits @@ -84,6 +84,7 @@ type CASBackendOpts struct { Description *string Provider CASBackendProvider Default *bool + Fallback *bool ValidationStatus CASBackendValidationStatus ValidationError *string } @@ -91,7 +92,6 @@ type CASBackendOpts struct { type CASBackendCreateOpts struct { *CASBackendOpts Name string - Fallback bool MaxBytes int64 } @@ -104,6 +104,7 @@ type CASBackendUpdateOpts struct { type CASBackendRepo interface { FindDefaultBackend(ctx context.Context, orgID uuid.UUID) (*CASBackend, error) FindFallbackBackend(ctx context.Context, orgID uuid.UUID) (*CASBackend, error) + FindInlineBackend(ctx context.Context, orgID uuid.UUID) (*CASBackend, error) FindByID(ctx context.Context, ID uuid.UUID) (*CASBackend, error) FindByIDInOrg(ctx context.Context, OrgID, ID uuid.UUID) (*CASBackend, error) FindByNameInOrg(ctx context.Context, OrgID uuid.UUID, name string) (*CASBackend, error) @@ -120,6 +121,8 @@ type CASBackendRepo interface { type CASBackendReader interface { FindDefaultBackend(ctx context.Context, orgID string) (*CASBackend, error) + FindFallbackBackend(ctx context.Context, orgID string) (*CASBackend, error) + FindDefaultOrFallbackBackend(ctx context.Context, orgID string) (*CASBackend, error) FindByIDInOrg(ctx context.Context, OrgID, ID string) (*CASBackend, error) PerformValidation(ctx context.Context, ID string) error } @@ -233,7 +236,42 @@ func (uc *CASBackendUseCase) FindFallbackBackend(ctx context.Context, orgID stri return backend, nil } -func (uc *CASBackendUseCase) CreateInlineFallbackBackend(ctx context.Context, orgID string) (*CASBackend, error) { +// FindDefaultOrFallbackBackend finds a valid CAS backend for the organization. +// Attempts to use the default backend first, if invalid it uses the fallback backend. +func (uc *CASBackendUseCase) FindDefaultOrFallbackBackend(ctx context.Context, orgID string) (*CASBackend, error) { + // Find the default backend + defaultBackend, err := uc.FindDefaultBackend(ctx, orgID) + if err != nil { + return nil, err + } + + // Check if default backend is valid + if defaultBackend.ValidationStatus == CASBackendValidationOK { + return defaultBackend, nil + } + + // Default backend is invalid, try fallback + uc.logger.Infow("msg", "default CAS backend validation failed, attempting fallback", + "backend", defaultBackend.Name, "status", defaultBackend.ValidationStatus, "orgID", orgID) + + fallbackBackend, err := uc.FindFallbackBackend(ctx, orgID) + if err != nil { + if IsNotFound(err) { + return nil, NewErrValidationStr("default CAS backend is unreachable and no fallback backend is configured") + } + return nil, err + } + + // Check if fallback backend is valid + if fallbackBackend.ValidationStatus != CASBackendValidationOK { + return nil, NewErrValidationStr("both default and fallback CAS backends are unreachable") + } + + uc.logger.Infow("msg", "using fallback CAS backend", "backend", fallbackBackend.Name, "orgID", orgID) + return fallbackBackend, nil +} + +func (uc *CASBackendUseCase) CreateInlineBackend(ctx context.Context, orgID string) (*CASBackend, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -241,18 +279,19 @@ func (uc *CASBackendUseCase) CreateInlineFallbackBackend(ctx context.Context, or return uc.repo.Create(ctx, &CASBackendCreateOpts{ Name: "default-inline", - Fallback: true, MaxBytes: CASBackendInlineDefaultMaxBytes, CASBackendOpts: &CASBackendOpts{ - Provider: CASBackendInline, Default: ToPtr(true), + Provider: CASBackendInline, + Default: ToPtr(true), + Fallback: ToPtr(false), Description: &CASBackendInlineDescription, OrgID: orgUUID, }, }) } -// Set fallback backend as default -func (uc *CASBackendUseCase) defaultFallbackBackend(ctx context.Context, orgID string) (*CASBackend, error) { +// promoteNextAvailableBackend promotes the next available backend to default. +func (uc *CASBackendUseCase) promoteNextAvailableBackend(ctx context.Context, orgID string) (*CASBackend, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -262,14 +301,20 @@ func (uc *CASBackendUseCase) defaultFallbackBackend(ctx context.Context, orgID s if err != nil { return nil, err } else if backend == nil { - // If there is no fallback backend, we skip the update - return nil, nil + // If there is no fallback backend, try to find and use inline backend + inlineBackend, err := uc.repo.FindInlineBackend(ctx, orgUUID) + if err != nil { + return nil, err + } else if inlineBackend == nil { + return nil, nil + } + return uc.repo.Update(ctx, &CASBackendUpdateOpts{ID: inlineBackend.ID, CASBackendOpts: &CASBackendOpts{Default: ToPtr(true)}}) } return uc.repo.Update(ctx, &CASBackendUpdateOpts{ID: backend.ID, CASBackendOpts: &CASBackendOpts{Default: ToPtr(true)}}) } -func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, name, location, description string, provider CASBackendProvider, creds any, defaultB bool, maxBytes *int64) (*CASBackend, error) { +func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, name, location, description string, provider CASBackendProvider, creds any, defaultB bool, fallbackB bool, maxBytes *int64) (*CASBackend, error) { if orgID == "" || name == "" { return nil, NewErrValidationStr("organization and name are required") } @@ -302,7 +347,7 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, name, location, MaxBytes: finalMaxBytes, Name: name, CASBackendOpts: &CASBackendOpts{ - Location: location, SecretName: secretName, Provider: provider, Default: ToPtr(defaultB), + Location: location, SecretName: secretName, Provider: provider, Default: ToPtr(defaultB), Fallback: ToPtr(fallbackB), Description: &description, OrgID: orgUUID, }, @@ -332,8 +377,8 @@ func (uc *CASBackendUseCase) Create(ctx context.Context, orgID, name, location, return backend, nil } -// Update will update credentials, description, default status, or max bytes -func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id string, description *string, creds any, defaultB *bool, maxBytes *int64) (*CASBackend, error) { +// Update will update credentials, description, default status, fallback status, or max bytes +func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id string, description *string, creds any, defaultB *bool, fallbackB *bool, maxBytes *int64) (*CASBackend, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -380,6 +425,7 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id string, descr CASBackendOpts: &CASBackendOpts{ SecretName: secretName, Default: defaultB, + Fallback: fallbackB, Description: description, OrgID: orgUUID, }, @@ -412,10 +458,10 @@ func (uc *CASBackendUseCase) Update(ctx context.Context, orgID, id string, descr } } - // If we just updated the backend from default=true => default=false, we need to set up the fallback as default + // If we just updated the backend from default=true => default=false, we need to promote the next available backend if before.Default && !after.Default { - if _, err := uc.defaultFallbackBackend(ctx, orgID); err != nil { - return nil, fmt.Errorf("setting the fallback backend as default: %w", err) + if _, err := uc.promoteNextAvailableBackend(ctx, orgID); err != nil { + return nil, fmt.Errorf("promoting next available backend to default: %w", err) } } @@ -502,18 +548,19 @@ func (uc *CASBackendUseCase) SoftDelete(ctx context.Context, orgID, id string) e return NewErrNotFound("CAS Backend") } - if backend.Fallback { - return NewErrValidation(errors.New("can't delete the fallback CAS backend")) + // Prevent deletion of inline backend + if backend.Provider == CASBackendInline { + return NewErrValidation(errors.New("can't delete the inline CAS backend")) } if err := uc.repo.SoftDelete(ctx, backendUUID); err != nil { return err } - // If we just deleted the default backend, we need to set up the fallback as default + // If we just deleted the default backend, we need to promote the next available backend if backend.Default { - if _, err := uc.defaultFallbackBackend(ctx, orgID); err != nil { - return fmt.Errorf("setting the fallback backend as default: %w", err) + if _, err := uc.promoteNextAvailableBackend(ctx, orgID); err != nil { + return fmt.Errorf("promoting next available backend to default: %w", err) } } diff --git a/app/controlplane/pkg/biz/casbackend_checker.go b/app/controlplane/pkg/biz/casbackend_checker.go index 1dd5af32b..f9dd35ae9 100644 --- a/app/controlplane/pkg/biz/casbackend_checker.go +++ b/app/controlplane/pkg/biz/casbackend_checker.go @@ -39,7 +39,7 @@ type CASBackendChecker struct { type CASBackendCheckerOpts struct { // Whether to check only default backends or all backends - OnlyDefaults *bool + OnlyDefaultsOrFallbacks *bool // Interval between checks, defaults to 30 minutes CheckInterval time.Duration // Timeout for each individual backend validation, defaults to 10 seconds @@ -66,9 +66,9 @@ func (c *CASBackendChecker) Start(ctx context.Context, opts *CASBackendCheckerOp interval = opts.CheckInterval } - onlyDefaults := true - if opts != nil && opts.OnlyDefaults != nil { - onlyDefaults = *opts.OnlyDefaults + onlyDefaultsOrFallbacks := true + if opts != nil && opts.OnlyDefaultsOrFallbacks != nil { + onlyDefaultsOrFallbacks = *opts.OnlyDefaultsOrFallbacks } // Apply validation timeout from options if provided @@ -82,7 +82,7 @@ func (c *CASBackendChecker) Start(ctx context.Context, opts *CASBackendCheckerOp initialDelay = opts.InitialDelay } - c.logger.Infow("msg", "CAS backend checker configured", "initialDelay", initialDelay, "interval", interval, "allBackends", !onlyDefaults, "timeout", c.validationTimeout) + c.logger.Infow("msg", "CAS backend checker configured", "initialDelay", initialDelay, "interval", interval, "allBackends", !onlyDefaultsOrFallbacks, "timeout", c.validationTimeout) select { case <-ctx.Done(): @@ -93,7 +93,7 @@ func (c *CASBackendChecker) Start(ctx context.Context, opts *CASBackendCheckerOp } // Run first check - if err := c.checkBackends(ctx, onlyDefaults); err != nil { + if err := c.checkBackends(ctx, onlyDefaultsOrFallbacks); err != nil { c.logger.Errorf("initial CAS backend check failed: %v", err) } @@ -107,7 +107,7 @@ func (c *CASBackendChecker) Start(ctx context.Context, opts *CASBackendCheckerOp c.logger.Info("CAS backend checker stopping due to context cancellation") return case <-ticker.C: - if err := c.checkBackends(ctx, onlyDefaults); err != nil { + if err := c.checkBackends(ctx, onlyDefaultsOrFallbacks); err != nil { c.logger.Errorf("periodic CAS backend check failed: %v", err) } } diff --git a/app/controlplane/pkg/biz/casbackend_integration_test.go b/app/controlplane/pkg/biz/casbackend_integration_test.go index d178f91c0..cc2f86a9f 100644 --- a/app/controlplane/pkg/biz/casbackend_integration_test.go +++ b/app/controlplane/pkg/biz/casbackend_integration_test.go @@ -81,7 +81,7 @@ func (s *CASBackendIntegrationTestSuite) TestUniqueNameDuringCreate() { orgID = tc.opts.OrgID.String() } - got, err := s.CASBackend.Create(context.Background(), orgID, tc.opts.Name, location, description, backendType, nil, true, nil) + got, err := s.CASBackend.Create(context.Background(), orgID, tc.opts.Name, location, description, backendType, nil, true, false, nil) if tc.wantErrMsg != "" { s.ErrorContains(err, tc.wantErrMsg) return @@ -100,13 +100,13 @@ func (s *CASBackendIntegrationTestSuite) TestCreate() { s.Run("non-existing org", func() { _, err := s.CASBackend.Create( - context.TODO(), uuid.NewString(), randomName(), location, description, backendType, nil, true, nil, + context.TODO(), uuid.NewString(), randomName(), location, description, backendType, nil, true, false, nil, ) assert.Error(err) }) s.Run("create default", func() { - b, err := s.CASBackend.Create(context.TODO(), orgID, "my-name", location, description, backendType, nil, true, nil) + b, err := s.CASBackend.Create(context.TODO(), orgID, "my-name", location, description, backendType, nil, true, false, nil) assert.NoError(err) if diff := cmp.Diff(&biz.CASBackend{ @@ -128,8 +128,8 @@ func (s *CASBackendIntegrationTestSuite) TestCreate() { } }) - s.Run("create fallback", func() { - b, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), orgID) + s.Run("create inline backend", func() { + b, err := s.CASBackend.CreateInlineBackend(context.TODO(), orgID) assert.NoError(err) if diff := cmp.Diff(&biz.CASBackend{ @@ -138,7 +138,7 @@ func (s *CASBackendIntegrationTestSuite) TestCreate() { Name: "default-inline", Default: true, Inline: true, - Fallback: true, + Fallback: false, ValidationStatus: "OK", Limits: &biz.CASBackendLimits{ MaxBytes: 512000, @@ -149,31 +149,81 @@ func (s *CASBackendIntegrationTestSuite) TestCreate() { assert.Failf("mismatch (-want +got):\n%s", diff) } }) + + s.Run("create fallback backend", func() { + b, err := s.CASBackend.Create(context.TODO(), orgID, "my-fallback", location, description, backendType, nil, false, true, nil) + assert.NoError(err) + + if diff := cmp.Diff(&biz.CASBackend{ + Location: location, + Name: "my-fallback", + Description: description, + SecretName: "stored-OCI-secret", + Provider: backendType, + ValidationStatus: "OK", + Default: false, + Fallback: true, + Inline: false, + Limits: &biz.CASBackendLimits{ + MaxBytes: 104857600, + }, + }, b, + cmpopts.IgnoreFields(biz.CASBackend{}, "CreatedAt", "ID", "ValidatedAt", "UpdatedAt", "OrganizationID"), + ); diff != "" { + assert.Failf("mismatch (-want +got):\n%s", diff) + } + }) + + s.Run("cannot create backend as both default and fallback", func() { + _, err := s.CASBackend.Create(context.TODO(), orgID, "both-flags", location, description, backendType, nil, true, true, nil) + assert.Error(err) + assert.Contains(err.Error(), "cannot be both default and fallback") + }) } func (s *CASBackendIntegrationTestSuite) TestCreateOverride() { assert := assert.New(s.T()) - // When a new default backend is created, the previous default should be overridden - b1, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) - assert.NoError(err) - assert.True(b1.Default) - b2, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, true, nil) - assert.NoError(err) - assert.True(b2.Default) + s.Run("override default backend", func() { + // When a new default backend is created, the previous default should be overridden + b1, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) + assert.NoError(err) + assert.True(b1.Default) + b2, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, true, false, nil) + assert.NoError(err) + assert.True(b2.Default) - // Check that the first one is no longer default - b1, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, b1.ID.String()) - assert.NoError(err) - assert.False(b1.Default) + // Check that the first one is no longer default + b1, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, b1.ID.String()) + assert.NoError(err) + assert.False(b1.Default) + }) + + s.Run("override fallback backend", func() { + // When a new fallback backend is created, the previous fallback should be overridden + f1, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, false, true, nil) + assert.NoError(err) + assert.True(f1.Fallback) + assert.False(f1.Default) + + f2, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, false, true, nil) + assert.NoError(err) + assert.True(f2.Fallback) + assert.False(f2.Default) + + // Check that the first one is no longer fallback + f1, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, f1.ID.String()) + assert.NoError(err) + assert.False(f1.Fallback) + }) } func (s *CASBackendIntegrationTestSuite) TestFindByNameInOrg() { name1 := randomName() name2 := randomName() - b1, err := s.CASBackend.Create(context.TODO(), s.orgOne.ID, name1, location, description, backendType, nil, true, nil) + b1, err := s.CASBackend.Create(context.TODO(), s.orgOne.ID, name1, location, description, backendType, nil, true, false, nil) s.NoError(err) s.True(b1.Default) - b2, err := s.CASBackend.Create(context.TODO(), s.orgTwo.ID, name2, "another-location", description, backendType, nil, true, nil) + b2, err := s.CASBackend.Create(context.TODO(), s.orgTwo.ID, name2, "another-location", description, backendType, nil, true, false, nil) s.NoError(err) testCases := []struct { @@ -236,15 +286,15 @@ func (s *CASBackendIntegrationTestSuite) TestUpdate() { s.Run("overrides previous backends", func() { // When a new default backend is set, the previous default should be overridden - defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) assert.NoError(err) assert.True(defaultB.Default) - nonDefaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, false, nil) + nonDefaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, false, false, nil) assert.NoError(err) assert.False(nonDefaultB.Default) // Update the non-default to be default - nonDefaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, nonDefaultB.ID.String(), toPtrS(""), nil, toPtrBool(true), nil) + nonDefaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, nonDefaultB.ID.String(), toPtrS(""), nil, toPtrBool(true), nil, nil) assert.NoError(err) assert.True(nonDefaultB.Default) @@ -256,12 +306,12 @@ func (s *CASBackendIntegrationTestSuite) TestUpdate() { s.Run("can update only the description", func() { // When a new default backend is set, the previous default should be overridden - defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) assert.NoError(err) assert.Equal(description, defaultB.Description) // Update the description - defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS("updated desc"), nil, toPtrBool(true), nil) + defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS("updated desc"), nil, toPtrBool(true), nil, nil) assert.NoError(err) assert.Equal("updated desc", defaultB.Description) assert.True(defaultB.Default) @@ -269,49 +319,90 @@ func (s *CASBackendIntegrationTestSuite) TestUpdate() { s.Run("can update only the status", func() { // When a new default backend is set, the previous default should be overridden - defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) assert.NoError(err) assert.Equal(description, defaultB.Description) // update the status - defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil) + defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil, nil) assert.NoError(err) assert.Equal(description, defaultB.Description) assert.False(defaultB.Default) }) - s.Run("the fallback backend will be set if default true => false", func() { - // When a new default backend is set, the previous default should be overridden - fallbackB, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), s.orgNoBackend.ID) + s.Run("the inline backend will be promoted if default is unset and no fallback exists", func() { + // Create an inline backend + inlineB, err := s.CASBackend.CreateInlineBackend(context.TODO(), s.orgNoBackend.ID) assert.NoError(err) - assert.True(fallbackB.Fallback) - assert.True(fallbackB.Default) + assert.False(inlineB.Fallback) + assert.True(inlineB.Default) // Create a new default backend - defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) assert.NoError(err) assert.False(defaultB.Fallback) // it's not fallback assert.True(defaultB.Default) - // The fallback now is no longer the default - fallbackB, err = s.CASBackend.FindFallbackBackend(context.TODO(), s.orgNoBackend.ID) + // The inline is no longer the default + inlineB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, inlineB.ID.String()) assert.NoError(err) - assert.False(fallbackB.Default) + assert.False(inlineB.Default) // update the status - defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil) + defaultB, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), nil, toPtrBool(false), nil, nil) assert.NoError(err) assert.False(defaultB.Default) - // The fallback is now the default - fallbackB, err = s.CASBackend.FindFallbackBackend(context.TODO(), s.orgNoBackend.ID) + // The inline is now the default + inlineB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, inlineB.ID.String()) assert.NoError(err) - assert.True(fallbackB.Default) + assert.True(inlineB.Default) + }) + + s.Run("can update backend to fallback", func() { + // Create a non-fallback backend + b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, false, false, nil) + assert.NoError(err) + assert.False(b.Fallback) + + // Update it to be fallback + b, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, b.ID.String(), toPtrS(description), nil, nil, toPtrBool(true), nil) + assert.NoError(err) + assert.True(b.Fallback) + assert.False(b.Default) + }) + + s.Run("updating to default unsets fallback", func() { + // Create a fallback backend + b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, false, true, nil) + assert.NoError(err) + assert.True(b.Fallback) + assert.False(b.Default) + + // Update it to be default + b, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, b.ID.String(), toPtrS(description), nil, toPtrBool(true), nil, nil) + assert.NoError(err) + assert.True(b.Default) + assert.False(b.Fallback) + }) + + s.Run("updating to fallback unsets default", func() { + // Create a default backend + b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) + assert.NoError(err) + assert.True(b.Default) + assert.False(b.Fallback) + + // Update it to be fallback + b, err = s.CASBackend.Update(context.TODO(), s.orgNoBackend.ID, b.ID.String(), toPtrS(description), nil, nil, toPtrBool(true), nil) + assert.NoError(err) + assert.False(b.Default) + assert.True(b.Fallback) }) s.Run("can rotate credentials", func() { // When a new default backend is set, the previous default should be overridden - defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) assert.NoError(err) assert.Equal(description, defaultB.Description) @@ -322,7 +413,7 @@ func (s *CASBackendIntegrationTestSuite) TestUpdate() { s.credsWriter.On("SaveCredentials", ctx, s.orgNoBackend.ID, creds).Return("new-secret", nil) s.credsWriter.On("ReadCredentials", ctx, "new-secret", mock.Anything).Return(nil) s.backendProvider.On("ValidateAndExtractCredentials", location, mock.Anything).Return(nil, nil) - defaultB, err = s.CASBackend.Update(ctx, s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), creds, nil, nil) + defaultB, err = s.CASBackend.Update(ctx, s.orgNoBackend.ID, defaultB.ID.String(), toPtrS(description), creds, nil, nil, nil) assert.NoError(err) assert.Equal(description, defaultB.Description) assert.Equal("new-secret", defaultB.SecretName) @@ -334,60 +425,102 @@ func (s *CASBackendIntegrationTestSuite) TestSoftDelete() { assert := assert.New(s.T()) ctx := context.TODO() - backends, err := s.CASBackend.List(ctx, s.orgTwo.ID) - assert.NoError(err) - // There are two backends - require.Len(s.T(), backends, 2) + s.Run("delete default backend", func() { + backends, err := s.CASBackend.List(ctx, s.orgTwo.ID) + assert.NoError(err) + // There are two backends + require.Len(s.T(), backends, 2) - // We are going to delete the default one - toDelete := backends[1].ID - assert.True(backends[1].Default) + // We are going to delete the default one + toDelete := backends[1].ID + assert.True(backends[1].Default) - // Delete it - err = s.CASBackend.SoftDelete(ctx, s.orgTwo.ID, toDelete.String()) - assert.NoError(err) + // Delete it + err = s.CASBackend.SoftDelete(ctx, s.orgTwo.ID, toDelete.String()) + assert.NoError(err) - // there is one left - backends, err = s.CASBackend.List(ctx, s.orgTwo.ID) - assert.NoError(err) - // There is one backend - require.Len(s.T(), backends, 1) - assert.Equal(backends[0].ID, s.casBackend3.ID) + // there is one left + backends, err = s.CASBackend.List(ctx, s.orgTwo.ID) + assert.NoError(err) + // There is one backend + require.Len(s.T(), backends, 1) + assert.Equal(backends[0].ID, s.casBackend3.ID) + + // the deleted one can not be found by ID either + _, err = s.CASBackend.FindByIDInOrg(ctx, s.orgTwo.ID, toDelete.String()) + assert.ErrorAs(err, &biz.ErrNotFound{}) + + // the deleted one can not be found by as default + _, err = s.CASBackend.FindDefaultBackend(ctx, s.orgTwo.ID) + assert.ErrorAs(err, &biz.ErrNotFound{}) + }) + + s.Run("delete fallback backend", func() { + // Create a fallback backend + fallbackB, err := s.CASBackend.Create(ctx, s.orgNoBackend.ID, randomName(), location, description, backendType, nil, false, true, nil) + assert.NoError(err) + assert.True(fallbackB.Fallback) - // the deleted one can not be found by ID either - _, err = s.CASBackend.FindByIDInOrg(ctx, s.orgTwo.ID, toDelete.String()) - assert.ErrorAs(err, &biz.ErrNotFound{}) + // Try to delete it + err = s.CASBackend.SoftDelete(ctx, s.orgNoBackend.ID, fallbackB.ID.String()) + assert.NoError(err) - // the deleted one can not be found by as default - _, err = s.CASBackend.FindDefaultBackend(ctx, s.orgTwo.ID) - assert.ErrorAs(err, &biz.ErrNotFound{}) + // Verify it's deleted + _, err = s.CASBackend.FindByIDInOrg(ctx, s.orgNoBackend.ID, fallbackB.ID.String()) + assert.Error(err) + }) } -func (s *CASBackendIntegrationTestSuite) TestSoftDeleteFallbackOverride() { +func (s *CASBackendIntegrationTestSuite) TestSoftDeleteFallbackPromotion() { assert := assert.New(s.T()) - // We have two backends, one is fallback and another is default - fallbackB, err := s.CASBackend.CreateInlineFallbackBackend(context.TODO(), s.orgNoBackend.ID) - assert.NoError(err) - assert.True(fallbackB.Default) - // When a new default backend is set, the previous default should be overridden - b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, nil) - assert.NoError(err) + s.Run("fallback promoted when default is deleted", func() { + // Create a default backend + defaultB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) + assert.NoError(err) + assert.True(defaultB.Default) - // The fallback is not the default anymore - fallbackB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, fallbackB.ID.String()) - assert.NoError(err) - assert.False(fallbackB.Default) + // Create a fallback backend + fallbackB, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), "another-location", description, backendType, nil, false, true, nil) + assert.NoError(err) + assert.True(fallbackB.Fallback) + assert.False(fallbackB.Default) - // Once we delete the default, the fallback should be the default again - // Delete it - err = s.CASBackend.SoftDelete(context.TODO(), s.orgNoBackend.ID, b.ID.String()) - assert.NoError(err) + // Delete the default + err = s.CASBackend.SoftDelete(context.TODO(), s.orgNoBackend.ID, defaultB.ID.String()) + assert.NoError(err) - // The fallback is NOW THE DEFAULT - fallbackB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, fallbackB.ID.String()) - assert.NoError(err) - assert.True(fallbackB.Default) + // The fallback should now be promoted to default + fallbackB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, fallbackB.ID.String()) + assert.NoError(err) + assert.True(fallbackB.Default) + }) + + s.Run("inline promoted when default is deleted and no fallback", func() { + // We have two backends, one is inline and another is default + inlineB, err := s.CASBackend.CreateInlineBackend(context.TODO(), s.orgNoBackend.ID) + assert.NoError(err) + assert.True(inlineB.Default) + + // When a new default backend is set, the previous default should be overridden + b, err := s.CASBackend.Create(context.TODO(), s.orgNoBackend.ID, randomName(), location, description, backendType, nil, true, false, nil) + assert.NoError(err) + + // The inline is not the default anymore + inlineB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, inlineB.ID.String()) + assert.NoError(err) + assert.False(inlineB.Default) + + // Once we delete the default, the inline should be the default again + // Delete it + err = s.CASBackend.SoftDelete(context.TODO(), s.orgNoBackend.ID, b.ID.String()) + assert.NoError(err) + + // The inline is NOW THE DEFAULT + inlineB, err = s.CASBackend.FindByIDInOrg(context.TODO(), s.orgNoBackend.ID, inlineB.ID.String()) + assert.NoError(err) + assert.True(inlineB.Default) + }) } func (s *CASBackendIntegrationTestSuite) TestList() { @@ -460,11 +593,11 @@ func (s *CASBackendIntegrationTestSuite) SetupTest() { s.orgNoBackend, err = s.Organization.Create(ctx, "testing-org-3-no-backends") assert.NoError(err) - s.casBackend1, err = s.CASBackend.Create(ctx, s.orgOne.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, nil) + s.casBackend1, err = s.CASBackend.Create(ctx, s.orgOne.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, false, nil) assert.NoError(err) - s.casBackend2, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 2", "backend 2 description", backendType, nil, true, nil) + s.casBackend2, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 2", "backend 2 description", backendType, nil, true, false, nil) assert.NoError(err) - s.casBackend3, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 3", "backend 3 description", backendType, nil, false, nil) + s.casBackend3, err = s.CASBackend.Create(ctx, s.orgTwo.ID, randomName(), "my-location 3", "backend 3 description", backendType, nil, false, false, nil) assert.NoError(err) } diff --git a/app/controlplane/pkg/biz/casmapping_integration_test.go b/app/controlplane/pkg/biz/casmapping_integration_test.go index 902f9b033..c613c45ec 100644 --- a/app/controlplane/pkg/biz/casmapping_integration_test.go +++ b/app/controlplane/pkg/biz/casmapping_integration_test.go @@ -362,16 +362,16 @@ func (s *casMappingIntegrationSuite) SetupTest() { // Create casBackend in the database s.org1, err = s.Organization.Create(ctx, "testing-org-1-with-one0backend") assert.NoError(err) - s.casBackend1, err = s.CASBackend.Create(ctx, s.org1.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, nil) + s.casBackend1, err = s.CASBackend.Create(ctx, s.org1.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, false, nil) assert.NoError(err) s.org2, err = s.Organization.Create(ctx, "testing-org-2") assert.NoError(err) - s.casBackend2, err = s.CASBackend.Create(ctx, s.org2.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, nil) + s.casBackend2, err = s.CASBackend.Create(ctx, s.org2.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, false, nil) assert.NoError(err) // Create casBackend associated with an org which users are not member of s.orgNoUsers, err = s.Organization.Create(ctx, "org-without-users") assert.NoError(err) - s.casBackend3, err = s.CASBackend.Create(ctx, s.orgNoUsers.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, nil) + s.casBackend3, err = s.CASBackend.Create(ctx, s.orgNoUsers.ID, randomName(), "my-location", "backend 1 description", backendType, nil, true, false, nil) assert.NoError(err) // Create workflowRun in the database diff --git a/app/controlplane/pkg/biz/mocks/CASBackendRepo.go b/app/controlplane/pkg/biz/mocks/CASBackendRepo.go index ad516cfb5..c52c63565 100644 --- a/app/controlplane/pkg/biz/mocks/CASBackendRepo.go +++ b/app/controlplane/pkg/biz/mocks/CASBackendRepo.go @@ -516,6 +516,74 @@ func (_c *CASBackendRepo_FindFallbackBackend_Call) RunAndReturn(run func(ctx con return _c } +// FindInlineBackend provides a mock function for the type CASBackendRepo +func (_mock *CASBackendRepo) FindInlineBackend(ctx context.Context, orgID uuid.UUID) (*biz.CASBackend, error) { + ret := _mock.Called(ctx, orgID) + + if len(ret) == 0 { + panic("no return value specified for FindInlineBackend") + } + + var r0 *biz.CASBackend + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*biz.CASBackend, error)); ok { + return returnFunc(ctx, orgID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) *biz.CASBackend); ok { + r0 = returnFunc(ctx, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*biz.CASBackend) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = returnFunc(ctx, orgID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// CASBackendRepo_FindInlineBackend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindInlineBackend' +type CASBackendRepo_FindInlineBackend_Call struct { + *mock.Call +} + +// FindInlineBackend is a helper method to define mock.On call +// - ctx context.Context +// - orgID uuid.UUID +func (_e *CASBackendRepo_Expecter) FindInlineBackend(ctx interface{}, orgID interface{}) *CASBackendRepo_FindInlineBackend_Call { + return &CASBackendRepo_FindInlineBackend_Call{Call: _e.mock.On("FindInlineBackend", ctx, orgID)} +} + +func (_c *CASBackendRepo_FindInlineBackend_Call) Run(run func(ctx context.Context, orgID uuid.UUID)) *CASBackendRepo_FindInlineBackend_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *CASBackendRepo_FindInlineBackend_Call) Return(cASBackend *biz.CASBackend, err error) *CASBackendRepo_FindInlineBackend_Call { + _c.Call.Return(cASBackend, err) + return _c +} + +func (_c *CASBackendRepo_FindInlineBackend_Call) RunAndReturn(run func(ctx context.Context, orgID uuid.UUID) (*biz.CASBackend, error)) *CASBackendRepo_FindInlineBackend_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function for the type CASBackendRepo func (_mock *CASBackendRepo) List(ctx context.Context, orgID uuid.UUID) ([]*biz.CASBackend, error) { ret := _mock.Called(ctx, orgID) diff --git a/app/controlplane/pkg/biz/organization.go b/app/controlplane/pkg/biz/organization.go index 3a4bc1ab3..8d5a98c55 100644 --- a/app/controlplane/pkg/biz/organization.go +++ b/app/controlplane/pkg/biz/organization.go @@ -172,8 +172,8 @@ func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts . if options.createInlineBackend { // Create inline CAS-backend - if _, err := uc.casBackendUseCase.CreateInlineFallbackBackend(ctx, org.ID); err != nil { - return nil, fmt.Errorf("failed to create fallback backend: %w", err) + if _, err := uc.casBackendUseCase.CreateInlineBackend(ctx, org.ID); err != nil { + return nil, fmt.Errorf("failed to create inline backend: %w", err) } } diff --git a/app/controlplane/pkg/data/casbackend.go b/app/controlplane/pkg/data/casbackend.go index 73fccaf1c..617fb0727 100644 --- a/app/controlplane/pkg/data/casbackend.go +++ b/app/controlplane/pkg/data/casbackend.go @@ -81,6 +81,18 @@ func (r *CASBackendRepo) FindFallbackBackend(ctx context.Context, orgID uuid.UUI return entCASBackendToBiz(backend), nil } +// FindInlineBackend finds the inline CAS backend for the given organization +func (r *CASBackendRepo) FindInlineBackend(ctx context.Context, orgID uuid.UUID) (*biz.CASBackend, error) { + backend, err := orgScopedQuery(r.data.DB, orgID).QueryCasBackends().WithOrganization(). + Where(casbackend.ProviderEQ(biz.CASBackendInline), casbackend.DeletedAtIsNil()). + Only(ctx) + if err != nil && !ent.IsNotFound(err) { + return nil, err + } + + return entCASBackendToBiz(backend), nil +} + // Create creates a new CAS backend in the given organization // If it's set as default, it will unset the previous default backend func (r *CASBackendRepo) Create(ctx context.Context, opts *biz.CASBackendCreateOpts) (*biz.CASBackend, error) { @@ -89,6 +101,11 @@ func (r *CASBackendRepo) Create(ctx context.Context, opts *biz.CASBackendCreateO err error ) if err := WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { + // 0 - Prevent setting a backend as both default and fallback + if opts.Default != nil && *opts.Default && opts.Fallback != nil && *opts.Fallback { + return fmt.Errorf("a backend cannot be both default and fallback") + } + // 1 - unset default backend for all the other backends in the org if opts.Default != nil && *opts.Default { if err := tx.CASBackend.Update(). @@ -100,13 +117,24 @@ func (r *CASBackendRepo) Create(ctx context.Context, opts *biz.CASBackendCreateO } } - // 2 - create the new backend and set it as default if needed + // 2 - unset fallback backend for all the other backends in the org + if opts.Fallback != nil && *opts.Fallback { + if err := tx.CASBackend.Update(). + Where(casbackend.HasOrganizationWith(organization.ID(opts.OrgID))). + Where(casbackend.Fallback(true)). + SetFallback(false). + Exec(ctx); err != nil { + return fmt.Errorf("failed to clear previous fallback backend: %w", err) + } + } + + // 3 - create the new backend and set it as default/fallback if needed backend, err = tx.CASBackend.Create(). SetName(opts.Name). SetOrganizationID(opts.OrgID). SetLocation(opts.Location). SetNillableDescription(opts.Description). - SetFallback(opts.Fallback). + SetNillableFallback(opts.Fallback). SetProvider(opts.Provider). SetNillableDefault(opts.Default). SetSecretName(opts.SecretName). @@ -145,14 +173,39 @@ func (r *CASBackendRepo) Update(ctx context.Context, opts *biz.CASBackendUpdateO } } - // 2 - Chain the list of updates + // 2 - unset fallback backend for all the other backends in the org + if opts.Fallback != nil && *opts.Fallback { + if err := tx.CASBackend.Update(). + Where(casbackend.HasOrganizationWith(organization.ID(opts.OrgID))). + Where(casbackend.Fallback(true)). + SetFallback(false). + Exec(ctx); err != nil { + return fmt.Errorf("failed to clear previous fallback backend: %w", err) + } + } + + // 3 - Chain the list of updates // TODO: allow setting values as empty, currently it's not possible. // We do it in other models by providing pointers to string + setNillableX methods updateChain := tx.CASBackend.UpdateOneID(opts.ID). - SetNillableDefault(opts.Default). SetNillableDescription(opts.Description). SetUpdatedAt(time.Now()) + // Make default and fallback mutually exclusive + if opts.Default != nil { + updateChain = updateChain.SetDefault(*opts.Default) + if *opts.Default { + updateChain = updateChain.SetFallback(false) + } + } + + if opts.Fallback != nil { + updateChain = updateChain.SetFallback(*opts.Fallback) + if *opts.Fallback { + updateChain = updateChain.SetDefault(false) + } + } + // If secretName is provided we set it if opts.SecretName != "" { updateChain = updateChain.SetSecretName(opts.SecretName) @@ -253,7 +306,7 @@ func (r *CASBackendRepo) UpdateValidationStatus(ctx context.Context, id uuid.UUI } // ListBackends returns CAS backends across all organizations. Only not inline backends are returned -// If onlyDefaults is true, only default backends are returned +// If onlyDefaults is true, only default and fallback backends are returned func (r *CASBackendRepo) ListBackends(ctx context.Context, onlyDefaults bool) ([]*biz.CASBackend, error) { query := r.data.DB.CASBackend.Query(). WithOrganization(). @@ -265,7 +318,10 @@ func (r *CASBackendRepo) ListBackends(ctx context.Context, onlyDefaults bool) ([ ) if onlyDefaults { - query = query.Where(casbackend.Default(true)) + query = query.Where(casbackend.Or( + casbackend.Default(true), + casbackend.Fallback(true), + )) } backends, err := query.All(ctx) diff --git a/app/controlplane/pkg/data/ent/casbackend_create.go b/app/controlplane/pkg/data/ent/casbackend_create.go index afdf4fae6..edacbf94f 100644 --- a/app/controlplane/pkg/data/ent/casbackend_create.go +++ b/app/controlplane/pkg/data/ent/casbackend_create.go @@ -627,6 +627,18 @@ func (u *CASBackendUpsert) ClearDeletedAt() *CASBackendUpsert { return u } +// SetFallback sets the "fallback" field. +func (u *CASBackendUpsert) SetFallback(v bool) *CASBackendUpsert { + u.Set(casbackend.FieldFallback, v) + return u +} + +// UpdateFallback sets the "fallback" field to the value that was provided on create. +func (u *CASBackendUpsert) UpdateFallback() *CASBackendUpsert { + u.SetExcluded(casbackend.FieldFallback) + return u +} + // SetMaxBlobSizeBytes sets the "max_blob_size_bytes" field. func (u *CASBackendUpsert) SetMaxBlobSizeBytes(v int64) *CASBackendUpsert { u.Set(casbackend.FieldMaxBlobSizeBytes, v) @@ -674,9 +686,6 @@ func (u *CASBackendUpsertOne) UpdateNewValues() *CASBackendUpsertOne { if _, exists := u.create.mutation.CreatedAt(); exists { s.SetIgnore(casbackend.FieldCreatedAt) } - if _, exists := u.create.mutation.Fallback(); exists { - s.SetIgnore(casbackend.FieldFallback) - } })) return u } @@ -841,6 +850,20 @@ func (u *CASBackendUpsertOne) ClearDeletedAt() *CASBackendUpsertOne { }) } +// SetFallback sets the "fallback" field. +func (u *CASBackendUpsertOne) SetFallback(v bool) *CASBackendUpsertOne { + return u.Update(func(s *CASBackendUpsert) { + s.SetFallback(v) + }) +} + +// UpdateFallback sets the "fallback" field to the value that was provided on create. +func (u *CASBackendUpsertOne) UpdateFallback() *CASBackendUpsertOne { + return u.Update(func(s *CASBackendUpsert) { + s.UpdateFallback() + }) +} + // SetMaxBlobSizeBytes sets the "max_blob_size_bytes" field. func (u *CASBackendUpsertOne) SetMaxBlobSizeBytes(v int64) *CASBackendUpsertOne { return u.Update(func(s *CASBackendUpsert) { @@ -1057,9 +1080,6 @@ func (u *CASBackendUpsertBulk) UpdateNewValues() *CASBackendUpsertBulk { if _, exists := b.mutation.CreatedAt(); exists { s.SetIgnore(casbackend.FieldCreatedAt) } - if _, exists := b.mutation.Fallback(); exists { - s.SetIgnore(casbackend.FieldFallback) - } } })) return u @@ -1225,6 +1245,20 @@ func (u *CASBackendUpsertBulk) ClearDeletedAt() *CASBackendUpsertBulk { }) } +// SetFallback sets the "fallback" field. +func (u *CASBackendUpsertBulk) SetFallback(v bool) *CASBackendUpsertBulk { + return u.Update(func(s *CASBackendUpsert) { + s.SetFallback(v) + }) +} + +// UpdateFallback sets the "fallback" field to the value that was provided on create. +func (u *CASBackendUpsertBulk) UpdateFallback() *CASBackendUpsertBulk { + return u.Update(func(s *CASBackendUpsert) { + s.UpdateFallback() + }) +} + // SetMaxBlobSizeBytes sets the "max_blob_size_bytes" field. func (u *CASBackendUpsertBulk) SetMaxBlobSizeBytes(v int64) *CASBackendUpsertBulk { return u.Update(func(s *CASBackendUpsert) { diff --git a/app/controlplane/pkg/data/ent/casbackend_update.go b/app/controlplane/pkg/data/ent/casbackend_update.go index c5d35ab00..47f69d2ef 100644 --- a/app/controlplane/pkg/data/ent/casbackend_update.go +++ b/app/controlplane/pkg/data/ent/casbackend_update.go @@ -155,6 +155,20 @@ func (_u *CASBackendUpdate) ClearDeletedAt() *CASBackendUpdate { return _u } +// SetFallback sets the "fallback" field. +func (_u *CASBackendUpdate) SetFallback(v bool) *CASBackendUpdate { + _u.mutation.SetFallback(v) + return _u +} + +// SetNillableFallback sets the "fallback" field if the given value is not nil. +func (_u *CASBackendUpdate) SetNillableFallback(v *bool) *CASBackendUpdate { + if v != nil { + _u.SetFallback(*v) + } + return _u +} + // SetMaxBlobSizeBytes sets the "max_blob_size_bytes" field. func (_u *CASBackendUpdate) SetMaxBlobSizeBytes(v int64) *CASBackendUpdate { _u.mutation.ResetMaxBlobSizeBytes() @@ -334,6 +348,9 @@ func (_u *CASBackendUpdate) sqlSave(ctx context.Context) (_node int, err error) if _u.mutation.DeletedAtCleared() { _spec.ClearField(casbackend.FieldDeletedAt, field.TypeTime) } + if value, ok := _u.mutation.Fallback(); ok { + _spec.SetField(casbackend.FieldFallback, field.TypeBool, value) + } if value, ok := _u.mutation.MaxBlobSizeBytes(); ok { _spec.SetField(casbackend.FieldMaxBlobSizeBytes, field.TypeInt64, value) } @@ -558,6 +575,20 @@ func (_u *CASBackendUpdateOne) ClearDeletedAt() *CASBackendUpdateOne { return _u } +// SetFallback sets the "fallback" field. +func (_u *CASBackendUpdateOne) SetFallback(v bool) *CASBackendUpdateOne { + _u.mutation.SetFallback(v) + return _u +} + +// SetNillableFallback sets the "fallback" field if the given value is not nil. +func (_u *CASBackendUpdateOne) SetNillableFallback(v *bool) *CASBackendUpdateOne { + if v != nil { + _u.SetFallback(*v) + } + return _u +} + // SetMaxBlobSizeBytes sets the "max_blob_size_bytes" field. func (_u *CASBackendUpdateOne) SetMaxBlobSizeBytes(v int64) *CASBackendUpdateOne { _u.mutation.ResetMaxBlobSizeBytes() @@ -767,6 +798,9 @@ func (_u *CASBackendUpdateOne) sqlSave(ctx context.Context) (_node *CASBackend, if _u.mutation.DeletedAtCleared() { _spec.ClearField(casbackend.FieldDeletedAt, field.TypeTime) } + if value, ok := _u.mutation.Fallback(); ok { + _spec.SetField(casbackend.FieldFallback, field.TypeBool, value) + } if value, ok := _u.mutation.MaxBlobSizeBytes(); ok { _spec.SetField(casbackend.FieldMaxBlobSizeBytes, field.TypeInt64, value) } diff --git a/app/controlplane/pkg/data/ent/schema/casbackend.go b/app/controlplane/pkg/data/ent/schema/casbackend.go index c81840fe0..16652db59 100644 --- a/app/controlplane/pkg/data/ent/schema/casbackend.go +++ b/app/controlplane/pkg/data/ent/schema/casbackend.go @@ -58,8 +58,8 @@ func (CASBackend) Fields() []ent.Field { Annotations(&entsql.Annotation{Default: "CURRENT_TIMESTAMP"}), field.Bool("default").Default(false), field.Time("deleted_at").Optional(), - // fallback, main cas backend. If true, this backend will be used as a fallback and cannot be deleted - field.Bool("fallback").Default(false).Immutable(), + // fallback, main cas backend. If true, this backend will be used as a fallback when the default backend is unreachable + field.Bool("fallback").Default(false), field.Int64("max_blob_size_bytes"), } }