diff --git a/charts/controller/crds/deployments.plural.sh_deploymentsettings.yaml b/charts/controller/crds/deployments.plural.sh_deploymentsettings.yaml index 0b3d550f6e..9890917574 100644 --- a/charts/controller/crds/deployments.plural.sh_deploymentsettings.yaml +++ b/charts/controller/crds/deployments.plural.sh_deploymentsettings.yaml @@ -353,6 +353,48 @@ spec: x-kubernetes-map-type: atomic type: object type: object + vectorStore: + properties: + elastic: + properties: + host: + type: string + index: + type: string + passwordSecretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - host + - index + type: object + enabled: + default: false + type: boolean + vectorStore: + enum: + - ELASTIC + type: string + type: object vertex: description: Vertex holds configuration for using GCP VertexAI to generate LLM insights diff --git a/go/controller/api/v1alpha1/deploymentsettings_types.go b/go/controller/api/v1alpha1/deploymentsettings_types.go index adeb418f28..e0628a6234 100644 --- a/go/controller/api/v1alpha1/deploymentsettings_types.go +++ b/go/controller/api/v1alpha1/deploymentsettings_types.go @@ -332,6 +332,9 @@ type AISettings struct { // // +kubebuilder:validation:Optional Vertex *VertexSettings `json:"vertex,omitempty"` + + // +kubebuilder:validation:Optional + VectorStore *VectorStore `json:"vectorStore,omitempty"` } type Tools struct { @@ -369,11 +372,17 @@ func (in *LoggingSettings) Attributes(ctx context.Context, c client.Client, name } func (in *AISettings) Attributes(ctx context.Context, c client.Client, namespace string) (*console.AiSettingsAttributes, error) { + vectorStoreAttributes, err := in.VectorStore.Attributes(ctx, c, namespace) + if err != nil { + return nil, err + } + attr := &console.AiSettingsAttributes{ Enabled: in.Enabled, Provider: in.Provider, ToolProvider: in.ToolProvider, EmbeddingProvider: in.EmbeddingProvider, + VectorStore: vectorStoreAttributes, } if in.Tools != nil && in.Tools.CreatePr != nil { @@ -691,3 +700,79 @@ func (in *VertexSettings) ServiceAccountJSON(ctx context.Context, c client.Clien res, err := utils.GetSecretKey(ctx, c, in.ServiceAccountJsonSecretRef, namespace) return lo.ToPtr(res), err } + +type VectorStore struct { + // +kubebuilder:default=false + // +kubebuilder:validation:Optional + Enabled *bool `json:"enabled,omitempty"` + + // +kubebuilder:validation:Enum=ELASTIC + // +kubebuilder:validation:Optional + VectorStore *console.VectorStore `json:"vectorStore,omitempty"` + + // +kubebuilder:validation:Optional + Elastic *ElasticsearchConnectionSettings `json:"elastic,omitempty"` +} + +func (in *VectorStore) Attributes(ctx context.Context, c client.Client, namespace string) (*console.VectorStoreAttributes, error) { + if in == nil { + return nil, nil + } + + if lo.FromPtr(in.Enabled) && in.VectorStore == nil { + return nil, fmt.Errorf("vector store type has to be set if it is enabled") + } + + attr := &console.VectorStoreAttributes{ + Enabled: in.Enabled, + Store: in.VectorStore, + } + + switch *in.VectorStore { + case console.VectorStoreElastic: + if in.Elastic == nil { + return nil, fmt.Errorf("must provide elastic configuration to set the provider to ELASTIC") + } + + password, err := in.Elastic.Password(ctx, c, namespace) + if err != nil { + return nil, err + } + + attr.Elastic = &console.ElasticsearchConnectionAttributes{ + Host: in.Elastic.Host, + Index: in.Elastic.Index, + User: in.Elastic.User, + Password: password, + } + } + + return attr, nil +} + +type ElasticsearchConnectionSettings struct { + // +kubebuilder:validation:Required + Host string `json:"host"` + + // +kubebuilder:validation:Required + Index string `json:"index"` + + // +kubebuilder:validation:Optional + User *string `json:"user,omitempty"` + + // +kubebuilder:validation:Optional + PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"` +} + +func (in *ElasticsearchConnectionSettings) Password(ctx context.Context, c client.Client, namespace string) (*string, error) { + if in == nil { + return nil, fmt.Errorf("configured elastic settings cannot be nil") + } + + if in.PasswordSecretRef == nil { + return nil, nil + } + + res, err := utils.GetSecretKey(ctx, c, in.PasswordSecretRef, namespace) + return lo.ToPtr(res), err +} diff --git a/go/controller/api/v1alpha1/zz_generated.deepcopy.go b/go/controller/api/v1alpha1/zz_generated.deepcopy.go index 8efaafbef0..59097718ec 100644 --- a/go/controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/controller/api/v1alpha1/zz_generated.deepcopy.go @@ -122,6 +122,11 @@ func (in *AISettings) DeepCopyInto(out *AISettings) { *out = new(VertexSettings) (*in).DeepCopyInto(*out) } + if in.VectorStore != nil { + in, out := &in.VectorStore, &out.VectorStore + *out = new(VectorStore) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AISettings. @@ -1608,6 +1613,31 @@ func (in *ElasticsearchConnection) DeepCopy() *ElasticsearchConnection { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ElasticsearchConnectionSettings) DeepCopyInto(out *ElasticsearchConnectionSettings) { + *out = *in + if in.User != nil { + in, out := &in.User, &out.User + *out = new(string) + **out = **in + } + if in.PasswordSecretRef != nil { + in, out := &in.PasswordSecretRef, &out.PasswordSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElasticsearchConnectionSettings. +func (in *ElasticsearchConnectionSettings) DeepCopy() *ElasticsearchConnectionSettings { + if in == nil { + return nil + } + out := new(ElasticsearchConnectionSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Env) DeepCopyInto(out *Env) { *out = *in @@ -5717,6 +5747,36 @@ func (in *Tools) DeepCopy() *Tools { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VectorStore) DeepCopyInto(out *VectorStore) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.VectorStore != nil { + in, out := &in.VectorStore, &out.VectorStore + *out = new(client.VectorStore) + **out = **in + } + if in.Elastic != nil { + in, out := &in.Elastic, &out.Elastic + *out = new(ElasticsearchConnectionSettings) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VectorStore. +func (in *VectorStore) DeepCopy() *VectorStore { + if in == nil { + return nil + } + out := new(VectorStore) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VertexSettings) DeepCopyInto(out *VertexSettings) { *out = *in diff --git a/go/controller/config/crd/bases/deployments.plural.sh_deploymentsettings.yaml b/go/controller/config/crd/bases/deployments.plural.sh_deploymentsettings.yaml index 0b3d550f6e..9890917574 100644 --- a/go/controller/config/crd/bases/deployments.plural.sh_deploymentsettings.yaml +++ b/go/controller/config/crd/bases/deployments.plural.sh_deploymentsettings.yaml @@ -353,6 +353,48 @@ spec: x-kubernetes-map-type: atomic type: object type: object + vectorStore: + properties: + elastic: + properties: + host: + type: string + index: + type: string + passwordSecretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - host + - index + type: object + enabled: + default: false + type: boolean + vectorStore: + enum: + - ELASTIC + type: string + type: object vertex: description: Vertex holds configuration for using GCP VertexAI to generate LLM insights diff --git a/go/controller/docs/api.md b/go/controller/docs/api.md index 67c8b19184..51e34c7bda 100644 --- a/go/controller/docs/api.md +++ b/go/controller/docs/api.md @@ -56,6 +56,7 @@ _Appears in:_ | --- | --- | --- | --- | | `model` _string_ | Model is the LLM model name to use. | | Optional: {}
| | `toolModel` _string_ | Model to use for tool calling, which is less frequent and often requires more advanced reasoning | | Optional: {}
| +| `embeddingModel` _string_ | Model to use for generating embeddings | | Optional: {}
| | `baseUrl` _string_ | A custom base url to use, for reimplementations of the same API scheme (for instance Together.ai uses the OpenAI API spec) | | Optional: {}
| | `tokenSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | TokenSecretRef is a reference to the local secret holding the token to access
the configured AI provider. | | Required: {}
| @@ -77,12 +78,14 @@ _Appears in:_ | `tools` _[Tools](#tools)_ | | | Optional: {}
| | `provider` _[AiProvider](#aiprovider)_ | Provider defines which of the supported LLM providers should be used. | OPENAI | Enum: [OPENAI ANTHROPIC OLLAMA AZURE BEDROCK VERTEX]
Optional: {}
| | `toolProvider` _[AiProvider](#aiprovider)_ | Provider to use for tool calling, in case you want to use a different LLM more optimized to those tasks | | Enum: [OPENAI ANTHROPIC OLLAMA AZURE BEDROCK VERTEX]
Optional: {}
| +| `embeddingProvider` _[AiProvider](#aiprovider)_ | Provider to use for generating embeddings. Oftentimes foundational model providers do not have embeddings models, and it's better to simply use OpenAI. | | Enum: [OPENAI ANTHROPIC OLLAMA AZURE BEDROCK VERTEX]
Optional: {}
| | `openAI` _[AIProviderSettings](#aiprovidersettings)_ | OpenAI holds the OpenAI provider configuration. | | Optional: {}
| | `anthropic` _[AIProviderSettings](#aiprovidersettings)_ | Anthropic holds the Anthropic provider configuration. | | Optional: {}
| | `ollama` _[OllamaSettings](#ollamasettings)_ | Ollama holds configuration for a self-hosted Ollama deployment, more details available at https://github.com/ollama/ollama | | Optional: {}
| | `azure` _[AzureOpenAISettings](#azureopenaisettings)_ | Azure holds configuration for using AzureOpenAI to generate LLM insights | | Optional: {}
| | `bedrock` _[BedrockSettings](#bedrocksettings)_ | Bedrock holds configuration for using AWS Bedrock to generate LLM insights | | Optional: {}
| | `vertex` _[VertexSettings](#vertexsettings)_ | Vertex holds configuration for using GCP VertexAI to generate LLM insights | | Optional: {}
| +| `vectorStore` _[VectorStore](#vectorstore)_ | | | Optional: {}
| @@ -104,6 +107,7 @@ _Appears in:_ | `apiVersion` _string_ | The azure openai Data plane - inference api version to use, defaults to 2024-10-01-preview or the latest available | | Optional: {}
| | `model` _string_ | The OpenAi Model you wish to use. If not specified, Plural will provide a default | | Optional: {}
| | `toolModel` _string_ | Model to use for tool calling, which is less frequent and often requires more advanced reasoning | | Optional: {}
| +| `embeddingModel` _string_ | Model to use for generating embeddings | | Optional: {}
| | `tokenSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | TokenSecretRef is a reference to the local secret holding the token to access
the configured AI provider. | | Required: {}
| @@ -821,6 +825,8 @@ _Appears in:_ | `ai` _[AISettings](#aisettings)_ | AI settings specifies a configuration for LLM provider clients | | Optional: {}
| | `logging` _[LoggingSettings](#loggingsettings)_ | Settings for connections to log aggregation datastores | | Optional: {}
| | `cost` _[CostSettings](#costsettings)_ | Settings for managing Plural's cost management features | | Optional: {}
| +| `deploymentRepositoryRef` _[NamespacedName](#namespacedname)_ | pointer to the deployment GIT repository to use | | Optional: {}
| +| `scaffoldsRepositoryRef` _[NamespacedName](#namespacedname)_ | pointer to the Scaffolds GIT repository to use | | Optional: {}
| #### ElasticsearchConnection @@ -842,6 +848,25 @@ _Appears in:_ | `passwordSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | PasswordSecretRef selects a key of a password Secret | | Optional: {}
| +#### ElasticsearchConnectionSettings + + + + + + + +_Appears in:_ +- [VectorStore](#vectorstore) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `host` _string_ | | | Required: {}
| +| `index` _string_ | | | Required: {}
| +| `user` _string_ | | | Optional: {}
| +| `passwordSecretRef` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#secretkeyselector-v1-core)_ | | | Optional: {}
| + + @@ -1390,6 +1415,7 @@ with the addition of kubebuilder/json annotations for better schema support. _Appears in:_ +- [DeploymentSettingsSpec](#deploymentsettingsspec) - [ServiceHelm](#servicehelm) | Field | Description | Default | Validation | @@ -2918,6 +2944,24 @@ _Appears in:_ | `createPr` _[CreatePr](#createpr)_ | | | | +#### VectorStore + + + + + + + +_Appears in:_ +- [AISettings](#aisettings) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `enabled` _boolean_ | | false | Optional: {}
| +| `vectorStore` _[VectorStore](#vectorstore)_ | | | Enum: [ELASTIC]
Optional: {}
| +| `elastic` _[ElasticsearchConnectionSettings](#elasticsearchconnectionsettings)_ | | | Optional: {}
| + + #### VertexSettings diff --git a/go/controller/internal/controller/common.go b/go/controller/internal/controller/common.go index 28b0e38e1f..eab0ee8cd6 100644 --- a/go/controller/internal/controller/common.go +++ b/go/controller/internal/controller/common.go @@ -9,6 +9,7 @@ import ( "github.com/pluralsh/polly/algorithms" "github.com/samber/lo" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -40,8 +41,15 @@ var ( // If the result is set, then any potential error will be saved in a condition // and ignored in the return to avoid rate limiting. // +// If not found error is detected, then the result is automatically changed to +// wait for resources. +// // It is important that at least one from a result or an error have to be non-nil. func handleRequeue(result *ctrl.Result, err error, setCondition func(condition metav1.Condition)) (ctrl.Result, error) { + if err != nil && apierrors.IsNotFound(err) { + result = &waitForResources + } + utils.MarkCondition(setCondition, v1alpha1.SynchronizedConditionType, metav1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, defaultErrMessage(err, "")) return lo.FromPtr(result), lo.Ternary(result != nil, nil, err) diff --git a/go/controller/internal/controller/deploymentsettings_controller.go b/go/controller/internal/controller/deploymentsettings_controller.go index adc2ecb61c..28641c7b98 100644 --- a/go/controller/internal/controller/deploymentsettings_controller.go +++ b/go/controller/internal/controller/deploymentsettings_controller.go @@ -18,13 +18,13 @@ package controller import ( "context" - goerrors "errors" "fmt" "github.com/samber/lo" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,7 +37,6 @@ import ( "github.com/pluralsh/console/go/controller/internal/cache" consoleclient "github.com/pluralsh/console/go/controller/internal/client" "github.com/pluralsh/console/go/controller/internal/credentials" - operrors "github.com/pluralsh/console/go/controller/internal/errors" "github.com/pluralsh/console/go/controller/internal/utils" ) @@ -116,12 +115,9 @@ func (r *DeploymentSettingsReconciler) Reconcile(ctx context.Context, req ctrl.R logger.Info("upsert deployment settings", "name", settings.Name) attr, err := r.genDeploymentSettingsAttr(ctx, settings) if err != nil { - utils.MarkCondition(settings.SetCondition, v1alpha1.SynchronizedConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) - if goerrors.Is(err, operrors.ErrRetriable) || goerrors.Is(err, operrors.ErrReferenceNotFound) { - return requeue, nil - } - return ctrl.Result{}, err + return handleRequeue(nil, err, settings.SetCondition) } + _, err = r.ConsoleClient.UpdateDeploymentSettings(ctx, *attr) if err != nil { utils.MarkCondition(settings.SetCondition, v1alpha1.SynchronizedConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) @@ -177,10 +173,6 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con } if settings.Spec.AI != nil { ai, err := settings.Spec.AI.Attributes(ctx, r.Client, settings.Namespace) - if errors.IsNotFound(err) { - return nil, operrors.ErrReferenceNotFound - } - if err != nil { return nil, err } @@ -188,10 +180,6 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con } if settings.Spec.Logging != nil { logging, err := settings.Spec.Logging.Attributes(ctx, r.Client, settings.Namespace) - if errors.IsNotFound(err) { - return nil, operrors.ErrReferenceNotFound - } - if err != nil { return nil, err } @@ -210,9 +198,6 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con if settings.Spec.Stacks.ConnectionRef != nil { connection := &v1alpha1.ScmConnection{} if err := r.Get(ctx, types.NamespacedName{Name: settings.Spec.Stacks.ConnectionRef.Name, Namespace: settings.Spec.Stacks.ConnectionRef.Namespace}, connection); err != nil { - if errors.IsNotFound(err) { - return nil, operrors.ErrReferenceNotFound - } return nil, err } connectionID = connection.Status.ID @@ -235,9 +220,6 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con if settings.Spec.DeploymentRepositoryRef != nil { id, err := getGitRepoID(ctx, r.Client, *settings.Spec.DeploymentRepositoryRef) if err != nil { - if errors.IsNotFound(err) { - return nil, operrors.ErrReferenceNotFound - } return nil, err } attr.DeployerRepositoryID = id @@ -246,9 +228,6 @@ func (r *DeploymentSettingsReconciler) genDeploymentSettingsAttr(ctx context.Con if settings.Spec.ScaffoldsRepositoryRef != nil { id, err := getGitRepoID(ctx, r.Client, *settings.Spec.ScaffoldsRepositoryRef) if err != nil { - if errors.IsNotFound(err) { - return nil, operrors.ErrReferenceNotFound - } return nil, err } attr.ArtifactRepositoryID = id @@ -289,7 +268,7 @@ func (r *DeploymentSettingsReconciler) ensure(settings *v1alpha1.DeploymentSetti settings.Spec.Bindings.Git = bindings if req || req2 || req3 || req4 { - return operrors.ErrRetriable + return errors.NewNotFound(schema.GroupResource{}, "bindings") } return nil diff --git a/plural/helm/console/crds/deployments.plural.sh_deploymentsettings.yaml b/plural/helm/console/crds/deployments.plural.sh_deploymentsettings.yaml index 0b3d550f6e..9890917574 100644 --- a/plural/helm/console/crds/deployments.plural.sh_deploymentsettings.yaml +++ b/plural/helm/console/crds/deployments.plural.sh_deploymentsettings.yaml @@ -353,6 +353,48 @@ spec: x-kubernetes-map-type: atomic type: object type: object + vectorStore: + properties: + elastic: + properties: + host: + type: string + index: + type: string + passwordSecretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - host + - index + type: object + enabled: + default: false + type: boolean + vectorStore: + enum: + - ELASTIC + type: string + type: object vertex: description: Vertex holds configuration for using GCP VertexAI to generate LLM insights