diff --git a/docs/data-sources/app.md b/docs/data-sources/app.md index 2ffce32..47c1bd4 100644 --- a/docs/data-sources/app.md +++ b/docs/data-sources/app.md @@ -66,7 +66,6 @@ output "app_api_key" { ### Read-Only -- `api_key` (String, Sensitive) The API key for the app. - `cluster_id` (String) The Kubernetes cluster identifier where the app is deployed. - `cname` (String) The region identifier (cname) for the app. - `created_at` (String) The timestamp when the app was created. diff --git a/docs/data-sources/apps.md b/docs/data-sources/apps.md index ac6b783..9c781ce 100644 --- a/docs/data-sources/apps.md +++ b/docs/data-sources/apps.md @@ -84,7 +84,6 @@ output "found_app_id" { Read-Only: -- `api_key` (String, Sensitive) The API key for the app. - `cluster_id` (String) The Kubernetes cluster identifier where the app is deployed. - `cname` (String) The region identifier (cname) for the app. - `created_at` (String) The timestamp when the app was created. diff --git a/docs/resources/app.md b/docs/resources/app.md index 59ebacc..2e93683 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -158,6 +158,7 @@ resource "spiceai_app" "with_region_lookup" { ### Optional - `description` (String) A description of the app. +- `executor` (Attributes) Executor container configuration. (see [below for nested schema](#nestedatt--executor)) - `image` (String) Image name for the spiced container. - `image_tag` (String) The Spice.ai runtime image tag to use for deployments (e.g., `latest`, `v0.18.0`). - `node_group` (String) The node group for the app deployment. @@ -165,19 +166,82 @@ resource "spiceai_app" "with_region_lookup" { - `region` (String) The region for the app deployment. - `registry` (String) Registry for the spiced image. - `replicas` (Number) The number of replicas for the app. Must be between 0 and 10. +- `resources` (Attributes) Resource requests and limits for the app container. (see [below for nested schema](#nestedatt--resources)) - `spicepod` (String) The spicepod configuration as a YAML or JSON string. This defines the datasets, models, and other spicepod settings for the app. - `storage_claim_size_gb` (Number) The storage claim size in GB for the app. - `tags` (Map of String) Key-value tags for the app. -- `update_channel` (String) Update channel for the spicepod. Valid values are `stable`, `nightly`, `internal`, `internal-sandbox`. +- `update_channel` (String) Update channel for the spicepod. Valid values are `stable`, `preview`, `nightly`, and `internal`. - `visibility` (String) The visibility of the app. Valid values are `public` or `private`. Defaults to `private`. ### Read-Only -- `api_key` (String, Sensitive) The API key for the app. This is used to authenticate requests to the app's endpoints. - `cluster_id` (String) The Kubernetes cluster identifier where the app is deployed. - `created_at` (String) The timestamp when the app was created. - `id` (String) The unique identifier of the app. + +### Nested Schema for `executor` + +Optional: + +- `replicas` (Number) Number of executor replicas. +- `resources` (Attributes) (see [below for nested schema](#nestedatt--executor--resources)) + + +### Nested Schema for `executor.resources` + +Optional: + +- `limits` (Attributes) (see [below for nested schema](#nestedatt--executor--resources--limits)) +- `requests` (Attributes) (see [below for nested schema](#nestedatt--executor--resources--requests)) + + +### Nested Schema for `executor.resources.limits` + +Optional: + +- `cpu` (String) Whole-number vCPU limit, or `-` for no CPU limit. +- `ephemeral_storage` (String) Ephemeral storage limit in Gi (for example, `8Gi`). +- `memory` (String) Memory limit in Gi (for example, `16Gi`). + + + +### Nested Schema for `executor.resources.requests` + +Optional: + +- `cpu` (String) +- `memory` (String) + + + + + +### Nested Schema for `resources` + +Optional: + +- `limits` (Attributes) (see [below for nested schema](#nestedatt--resources--limits)) +- `requests` (Attributes) (see [below for nested schema](#nestedatt--resources--requests)) + + +### Nested Schema for `resources.limits` + +Optional: + +- `cpu` (String) Whole-number vCPU limit, or `-` for no CPU limit. +- `ephemeral_storage` (String) Ephemeral storage limit in Gi (for example, `8Gi`). +- `memory` (String) Memory limit in Gi (for example, `16Gi`). + + + +### Nested Schema for `resources.requests` + +Optional: + +- `cpu` (String) +- `memory` (String) + ## Import Import is supported using the following syntax: diff --git a/docs/resources/deployment.md b/docs/resources/deployment.md index 3121e5d..1cbc8d4 100644 --- a/docs/resources/deployment.md +++ b/docs/resources/deployment.md @@ -138,6 +138,7 @@ resource "spiceai_deployment" "production" { ### Optional - `branch` (String) Git branch name associated with this deployment. Changing this forces a new deployment to be created. +- `channel` (String) Override the deployment channel for this deployment. Valid values are `stable`. Changing this forces a new deployment to be created. - `commit_message` (String) Git commit message associated with this deployment. Changing this forces a new deployment to be created. - `commit_sha` (String) Git commit SHA associated with this deployment. Changing this forces a new deployment to be created. - `debug` (Boolean) Enable debug mode for this deployment. Changing this forces a new deployment to be created. diff --git a/internal/client/client.go b/internal/client/client.go index 4028775..ea2dfc6 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -168,41 +168,48 @@ type App struct { // AppConfig represents the configuration of an app. type AppConfig struct { - Spicepod interface{} `json:"spicepod,omitempty"` - Registry string `json:"registry,omitempty"` - Image string `json:"image,omitempty"` - ImageTag string `json:"image_tag,omitempty"` - UpdateChannel string `json:"update_channel,omitempty"` - Replicas int `json:"replicas,omitempty"` - Region string `json:"region,omitempty"` - NodeGroup string `json:"node_group,omitempty"` - StorageClaimSizeGB float64 `json:"storage_claim_size_gb,omitempty"` + Spicepod interface{} `json:"spicepod,omitempty"` + Registry string `json:"registry,omitempty"` + Image string `json:"image,omitempty"` + ImageTag string `json:"image_tag,omitempty"` + UpdateChannel string `json:"update_channel,omitempty"` + Replicas int `json:"replicas,omitempty"` + Resources *ContainerResources `json:"resources,omitempty"` + Executor *ExecutorConfig `json:"executor,omitempty"` + Region string `json:"region,omitempty"` + NodeGroup string `json:"node_group,omitempty"` + StorageClaimSizeGB float64 `json:"storage_claim_size_gb,omitempty"` } // CreateAppRequest represents the request to create an app. type CreateAppRequest struct { - Name string `json:"name"` - Cname string `json:"cname"` - Description string `json:"description,omitempty"` - Visibility string `json:"visibility,omitempty"` - Tags map[string]string `json:"tags,omitempty"` + Name string `json:"name"` + Cname string `json:"cname"` + Description string `json:"description,omitempty"` + Visibility string `json:"visibility,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Replicas *int `json:"replicas,omitempty"` + Resources *ContainerResources `json:"resources,omitempty"` + Executor *ExecutorConfig `json:"executor,omitempty"` } // UpdateAppRequest represents the request to update an app. type UpdateAppRequest struct { - Description string `json:"description,omitempty"` - Visibility string `json:"visibility,omitempty"` - ProductionBranch string `json:"production_branch,omitempty"` - Tags map[string]string `json:"tags,omitempty"` - Spicepod interface{} `json:"spicepod,omitempty"` - Registry string `json:"registry,omitempty"` - Image string `json:"image,omitempty"` - ImageTag string `json:"image_tag,omitempty"` - UpdateChannel string `json:"update_channel,omitempty"` - Replicas *int `json:"replicas,omitempty"` - NodeGroup string `json:"node_group,omitempty"` - Region string `json:"region,omitempty"` - StorageClaimSizeGB *float64 `json:"storage_claim_size_gb,omitempty"` + Description string `json:"description,omitempty"` + Visibility string `json:"visibility,omitempty"` + ProductionBranch string `json:"production_branch,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Spicepod interface{} `json:"spicepod,omitempty"` + Registry string `json:"registry,omitempty"` + Image string `json:"image,omitempty"` + ImageTag string `json:"image_tag,omitempty"` + UpdateChannel string `json:"update_channel,omitempty"` + Replicas *int `json:"replicas,omitempty"` + Resources *ContainerResources `json:"resources,omitempty"` + Executor *ExecutorConfig `json:"executor,omitempty"` + NodeGroup string `json:"node_group,omitempty"` + Region string `json:"region,omitempty"` + StorageClaimSizeGB *float64 `json:"storage_claim_size_gb,omitempty"` } // Deployment represents a Spice.ai deployment. @@ -223,9 +230,35 @@ type Deployment struct { CreatedBy int64 `json:"created_by,omitempty"` } +// ResourceRequests represents requested container resources. +type ResourceRequests struct { + CPU string `json:"cpu,omitempty"` + Memory string `json:"memory,omitempty"` +} + +// ResourceLimits represents container resource limits. +type ResourceLimits struct { + CPU string `json:"cpu,omitempty"` + Memory string `json:"memory,omitempty"` + EphemeralStorage string `json:"ephemeral-storage,omitempty"` +} + +// ContainerResources represents resource requests and limits for a container. +type ContainerResources struct { + Limits *ResourceLimits `json:"limits,omitempty"` + Requests *ResourceRequests `json:"requests,omitempty"` +} + +// ExecutorConfig represents executor container configuration. +type ExecutorConfig struct { + Replicas *int `json:"replicas,omitempty"` + Resources *ContainerResources `json:"resources,omitempty"` +} + // CreateDeploymentRequest represents the request to create a deployment. type CreateDeploymentRequest struct { ImageTag string `json:"image_tag,omitempty"` + Channel string `json:"channel,omitempty"` Replicas *int `json:"replicas,omitempty"` Branch string `json:"branch,omitempty"` CommitSHA string `json:"commit_sha,omitempty"` diff --git a/internal/provider/datasource_app.go b/internal/provider/datasource_app.go index b354bc1..1418908 100644 --- a/internal/provider/datasource_app.go +++ b/internal/provider/datasource_app.go @@ -59,7 +59,6 @@ type AppDataSourceModel struct { // Read-only attributes CreatedAt types.String `tfsdk:"created_at"` ClusterID types.String `tfsdk:"cluster_id"` - APIKey types.String `tfsdk:"api_key"` } func (d *AppDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -153,11 +152,6 @@ func (d *AppDataSource) Schema(ctx context.Context, req datasource.SchemaRequest MarkdownDescription: "The Kubernetes cluster identifier where the app is deployed.", Computed: true, }, - "api_key": schema.StringAttribute{ - MarkdownDescription: "The API key for the app.", - Computed: true, - Sensitive: true, - }, }, } } @@ -236,7 +230,6 @@ func (d *AppDataSource) mapAppToModel(data *AppDataSourceModel, app *client.App) data.CreatedAt = types.StringValue(app.CreatedAt) data.ClusterID = types.StringValue(app.ClusterID) - data.APIKey = types.StringValue(app.APIKey) // Map cname if app.Cname != "" { diff --git a/internal/provider/datasource_apps.go b/internal/provider/datasource_apps.go index 77ab7b7..500c019 100644 --- a/internal/provider/datasource_apps.go +++ b/internal/provider/datasource_apps.go @@ -64,7 +64,6 @@ type AppModel struct { // Read-only attributes CreatedAt types.String `tfsdk:"created_at"` ClusterID types.String `tfsdk:"cluster_id"` - APIKey types.String `tfsdk:"api_key"` } func (d *AppsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -163,11 +162,6 @@ func (d *AppsDataSource) Schema(ctx context.Context, req datasource.SchemaReques MarkdownDescription: "The Kubernetes cluster identifier where the app is deployed.", Computed: true, }, - "api_key": schema.StringAttribute{ - MarkdownDescription: "The API key for the app.", - Computed: true, - Sensitive: true, - }, }, }, }, @@ -229,7 +223,6 @@ func (d *AppsDataSource) mapAppToModel(app *client.App) AppModel { ProductionBranch: types.StringValue(app.ProductionBranch), CreatedAt: types.StringValue(app.CreatedAt), ClusterID: types.StringValue(app.ClusterID), - APIKey: types.StringValue(app.APIKey), } // Map tags diff --git a/internal/provider/resource_app.go b/internal/provider/resource_app.go index 5bb85fd..63be216 100644 --- a/internal/provider/resource_app.go +++ b/internal/provider/resource_app.go @@ -178,6 +178,8 @@ type AppResourceModel struct { ImageTag types.String `tfsdk:"image_tag"` UpdateChannel types.String `tfsdk:"update_channel"` Replicas types.Int64 `tfsdk:"replicas"` + Resources types.Object `tfsdk:"resources"` + Executor types.Object `tfsdk:"executor"` NodeGroup types.String `tfsdk:"node_group"` Region types.String `tfsdk:"region"` StorageClaimSizeGB types.Float64 `tfsdk:"storage_claim_size_gb"` @@ -185,7 +187,6 @@ type AppResourceModel struct { // Read-only attributes CreatedAt types.String `tfsdk:"created_at"` ClusterID types.String `tfsdk:"cluster_id"` - APIKey types.String `tfsdk:"api_key"` } func (r *AppResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -305,11 +306,11 @@ resource "spiceai_app" "example" { Computed: true, }, "update_channel": schema.StringAttribute{ - MarkdownDescription: "Update channel for the spicepod. Valid values are `stable`, `nightly`, `internal`, `internal-sandbox`.", + MarkdownDescription: "Update channel for the spicepod. Valid values are `stable`, `preview`, `nightly`, and `internal`.", Optional: true, Computed: true, Validators: []validator.String{ - stringvalidator.OneOf("stable", "nightly", "internal", "internal-sandbox"), + stringvalidator.OneOf("stable", "preview", "nightly", "internal"), }, }, "replicas": schema.Int64Attribute{ @@ -320,6 +321,104 @@ resource "spiceai_app" "example" { int64validator.Between(0, 10), }, }, + "resources": schema.SingleNestedAttribute{ + MarkdownDescription: "Resource requests and limits for the app container.", + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "limits": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Whole-number vCPU limit, or `-` for no CPU limit.", + }, + "memory": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Memory limit in Gi (for example, `16Gi`).", + }, + "ephemeral_storage": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Ephemeral storage limit in Gi (for example, `8Gi`).", + }, + }, + }, + "requests": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "memory": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + "executor": schema.SingleNestedAttribute{ + MarkdownDescription: "Executor container configuration.", + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "replicas": schema.Int64Attribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Number of executor replicas.", + Validators: []validator.Int64{ + int64validator.Between(0, 10), + }, + }, + "resources": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "limits": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Whole-number vCPU limit, or `-` for no CPU limit.", + }, + "memory": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Memory limit in Gi (for example, `16Gi`).", + }, + "ephemeral_storage": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Ephemeral storage limit in Gi (for example, `8Gi`).", + }, + }, + }, + "requests": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "memory": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, "node_group": schema.StringAttribute{ MarkdownDescription: "The node group for the app deployment.", Optional: true, @@ -351,14 +450,6 @@ resource "spiceai_app" "example" { stringplanmodifier.UseStateForUnknown(), }, }, - "api_key": schema.StringAttribute{ - Computed: true, - Sensitive: true, - MarkdownDescription: "The API key for the app. This is used to authenticate requests to the app's endpoints.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, }, } } @@ -539,6 +630,8 @@ func (r *AppResource) hasConfigAttributes(data *AppResourceModel) bool { !data.ImageTag.IsNull() || !data.UpdateChannel.IsNull() || !data.Replicas.IsNull() || + !data.Resources.IsNull() || + !data.Executor.IsNull() || !data.NodeGroup.IsNull() || !data.Region.IsNull() || !data.StorageClaimSizeGB.IsNull() || @@ -602,6 +695,14 @@ func (r *AppResource) buildUpdateRequest(data *AppResourceModel) *client.UpdateA updateReq.Replicas = &replicas } + if !data.Resources.IsNull() && !data.Resources.IsUnknown() { + updateReq.Resources = expandContainerResources(data.Resources) + } + + if !data.Executor.IsNull() && !data.Executor.IsUnknown() { + updateReq.Executor = expandExecutorConfig(data.Executor) + } + if !data.NodeGroup.IsNull() && !data.NodeGroup.IsUnknown() { updateReq.NodeGroup = data.NodeGroup.ValueString() } @@ -694,12 +795,6 @@ func (r *AppResource) mapAppToModel(data *AppResourceModel, app *client.App, pre data.CreatedAt = types.StringNull() } - if app.APIKey != "" { - data.APIKey = types.StringValue(app.APIKey) - } else { - data.APIKey = types.StringNull() - } - // Map config fields if available if app.Config != nil { if app.Config.Registry != "" { @@ -728,6 +823,9 @@ func (r *AppResource) mapAppToModel(data *AppResourceModel, app *client.App, pre data.Replicas = types.Int64Value(int64(app.Config.Replicas)) + data.Resources = flattenContainerResources(app.Config.Resources) + data.Executor = flattenExecutorConfig(app.Config.Executor) + if app.Config.NodeGroup != "" { data.NodeGroup = types.StringValue(app.Config.NodeGroup) } else { @@ -769,8 +867,245 @@ func (r *AppResource) mapAppToModel(data *AppResourceModel, app *client.App, pre data.ImageTag = types.StringNull() data.UpdateChannel = types.StringNull() data.Replicas = types.Int64Null() + data.Resources = types.ObjectNull(map[string]attr.Type{ + "limits": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + "ephemeral_storage": types.StringType, + }}, + "requests": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + }}, + }) + data.Executor = types.ObjectNull(map[string]attr.Type{ + "replicas": types.Int64Type, + "resources": types.ObjectType{AttrTypes: map[string]attr.Type{ + "limits": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + "ephemeral_storage": types.StringType, + }}, + "requests": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + }}, + }}, + }) data.NodeGroup = types.StringNull() data.StorageClaimSizeGB = types.Float64Null() data.Spicepod = SpicepodStringValue{StringValue: types.StringNull()} } } + +func expandContainerResources(value types.Object) *client.ContainerResources { + if value.IsNull() || value.IsUnknown() { + return nil + } + + result := &client.ContainerResources{} + hasValues := false + + attrs := value.Attributes() + + if limitsAttr, ok := attrs["limits"]; ok { + if limitsObj, ok := limitsAttr.(types.Object); ok && !limitsObj.IsNull() && !limitsObj.IsUnknown() { + limits := &client.ResourceLimits{} + limitsHasValues := false + + for name, attrValue := range limitsObj.Attributes() { + strValue, ok := attrValue.(types.String) + if !ok || strValue.IsNull() || strValue.IsUnknown() { + continue + } + + switch name { + case "cpu": + limits.CPU = strValue.ValueString() + limitsHasValues = true + case "memory": + limits.Memory = strValue.ValueString() + limitsHasValues = true + case "ephemeral_storage": + limits.EphemeralStorage = strValue.ValueString() + limitsHasValues = true + } + } + + if limitsHasValues { + result.Limits = limits + hasValues = true + } + } + } + + if requestsAttr, ok := attrs["requests"]; ok { + if requestsObj, ok := requestsAttr.(types.Object); ok && !requestsObj.IsNull() && !requestsObj.IsUnknown() { + requests := &client.ResourceRequests{} + requestsHasValues := false + + for name, attrValue := range requestsObj.Attributes() { + strValue, ok := attrValue.(types.String) + if !ok || strValue.IsNull() || strValue.IsUnknown() { + continue + } + + switch name { + case "cpu": + requests.CPU = strValue.ValueString() + requestsHasValues = true + case "memory": + requests.Memory = strValue.ValueString() + requestsHasValues = true + } + } + + if requestsHasValues { + result.Requests = requests + hasValues = true + } + } + } + + if !hasValues { + return nil + } + + return result +} + +func expandExecutorConfig(value types.Object) *client.ExecutorConfig { + if value.IsNull() || value.IsUnknown() { + return nil + } + + result := &client.ExecutorConfig{} + hasValues := false + + attrs := value.Attributes() + + if replicasAttr, ok := attrs["replicas"]; ok { + if replicasValue, ok := replicasAttr.(types.Int64); ok && !replicasValue.IsNull() && !replicasValue.IsUnknown() { + replicas := int(replicasValue.ValueInt64()) + result.Replicas = &replicas + hasValues = true + } + } + + if resourcesAttr, ok := attrs["resources"]; ok { + if resourcesObj, ok := resourcesAttr.(types.Object); ok && !resourcesObj.IsNull() && !resourcesObj.IsUnknown() { + result.Resources = expandContainerResources(resourcesObj) + if result.Resources != nil { + hasValues = true + } + } + } + + if !hasValues { + return nil + } + + return result +} + +func flattenContainerResources(resources *client.ContainerResources) types.Object { + containerResourcesType := map[string]attr.Type{ + "limits": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + "ephemeral_storage": types.StringType, + }}, + "requests": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + }}, + } + + if resources == nil { + return types.ObjectNull(containerResourcesType) + } + + limitsType := map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + "ephemeral_storage": types.StringType, + } + + requestsType := map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + } + + limitsValue := types.ObjectNull(limitsType) + if resources.Limits != nil { + limitsValue = types.ObjectValueMust( + limitsType, + map[string]attr.Value{ + "cpu": stringValueOrNull(resources.Limits.CPU), + "memory": stringValueOrNull(resources.Limits.Memory), + "ephemeral_storage": stringValueOrNull(resources.Limits.EphemeralStorage), + }, + ) + } + + requestsValue := types.ObjectNull(requestsType) + if resources.Requests != nil { + requestsValue = types.ObjectValueMust( + requestsType, + map[string]attr.Value{ + "cpu": stringValueOrNull(resources.Requests.CPU), + "memory": stringValueOrNull(resources.Requests.Memory), + }, + ) + } + + return types.ObjectValueMust( + containerResourcesType, + map[string]attr.Value{ + "limits": limitsValue, + "requests": requestsValue, + }, + ) +} + +func flattenExecutorConfig(executor *client.ExecutorConfig) types.Object { + executorType := map[string]attr.Type{ + "replicas": types.Int64Type, + "resources": types.ObjectType{AttrTypes: map[string]attr.Type{ + "limits": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + "ephemeral_storage": types.StringType, + }}, + "requests": types.ObjectType{AttrTypes: map[string]attr.Type{ + "cpu": types.StringType, + "memory": types.StringType, + }}, + }}, + } + + if executor == nil { + return types.ObjectNull(executorType) + } + + replicasValue := types.Int64Null() + if executor.Replicas != nil { + replicasValue = types.Int64Value(int64(*executor.Replicas)) + } + + return types.ObjectValueMust( + executorType, + map[string]attr.Value{ + "replicas": replicasValue, + "resources": flattenContainerResources(executor.Resources), + }, + ) +} + +func stringValueOrNull(value string) attr.Value { + if value == "" { + return types.StringNull() + } + + return types.StringValue(value) +} diff --git a/internal/provider/resource_deployment.go b/internal/provider/resource_deployment.go index fd5511e..f6462e6 100644 --- a/internal/provider/resource_deployment.go +++ b/internal/provider/resource_deployment.go @@ -42,6 +42,7 @@ type DeploymentResourceModel struct { AppID types.String `tfsdk:"app_id"` Triggers types.Map `tfsdk:"triggers"` ImageTag types.String `tfsdk:"image_tag"` + Channel types.String `tfsdk:"channel"` Replicas types.Int64 `tfsdk:"replicas"` Branch types.String `tfsdk:"branch"` CommitSHA types.String `tfsdk:"commit_sha"` @@ -138,6 +139,13 @@ resource "spiceai_deployment" "example" { stringplanmodifier.UseStateForUnknown(), }, }, + "channel": schema.StringAttribute{ + MarkdownDescription: "Override the deployment channel for this deployment. Valid values are `stable`. Changing this forces a new deployment to be created.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, "replicas": schema.Int64Attribute{ MarkdownDescription: "Override the number of replicas for this deployment. Must be between 0 and 10. If not specified, uses the app's configured replicas. Changing this forces a new deployment to be created.", Optional: true, @@ -262,6 +270,10 @@ func (r *DeploymentResource) Create(ctx context.Context, req resource.CreateRequ createReq.ImageTag = data.ImageTag.ValueString() } + if !data.Channel.IsNull() && !data.Channel.IsUnknown() { + createReq.Channel = data.Channel.ValueString() + } + if !data.Replicas.IsNull() && !data.Replicas.IsUnknown() { replicas := int(data.Replicas.ValueInt64()) createReq.Replicas = &replicas