Skip to content

Commit

Permalink
feat(COR-921): allow to create a cluster with karpenter feature (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
pggb25 committed Jul 2, 2024
1 parent 48b5f6d commit 250c8fb
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 23 deletions.
17 changes: 14 additions & 3 deletions docs/data-sources/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ data "qovery_cluster" "my_cluster" {
- Default: ``.
- `disk_size` (Number)
- `features` (Attributes) Features of the cluster. (see [below for nested schema](#nestedatt--features))
- `instance_type` (String) Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`, and not set for Karpenter-enabled clusters
- `kubernetes_mode` (String) Kubernetes mode of the cluster.
- Can be: `K3S`, `MANAGED`.
- Default: `MANAGED`.
- `max_running_nodes` (Number) Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters]
- `max_running_nodes` (Number) Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters]
- Must be: `>= 1`.
- Default: `10`.
- `min_running_nodes` (Number) Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters].
- `min_running_nodes` (Number) Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters].
- Must be: `>= 1`.
- Default: `3`.
- `routing_table` (Attributes Set) List of routes of the cluster. (see [below for nested schema](#nestedatt--routing_table))
Expand All @@ -43,7 +44,6 @@ data "qovery_cluster" "my_cluster" {
- `cloud_provider` (String) Cloud provider of the cluster.
- Can be: `AWS`, `GCP`, `ON_PREMISE`, `SCW`.
- `credentials_id` (String) Id of the credentials.
- `instance_type` (String) Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`
- `name` (String) Name of the cluster.
- `region` (String) Region of the cluster.

Expand All @@ -53,6 +53,7 @@ data "qovery_cluster" "my_cluster" {
Optional:

- `existing_vpc` (Attributes) Network configuration if you want to install qovery on an existing VPC (see [below for nested schema](#nestedatt--features--existing_vpc))
- `karpenter` (Attributes) Karpenter parameters if you want to use Karpenter on an EKS cluster (see [below for nested schema](#nestedatt--features--karpenter))
- `static_ip` (Boolean) Static IP (AWS only) [NOTE: can't be updated after creation].
- Default: `false`.
- `vpc_subnet` (String) Custom VPC subnet (AWS only) [NOTE: can't be updated after creation].
Expand Down Expand Up @@ -81,6 +82,16 @@ Optional:
- `rds_subnets_zone_c_ids` (List of String) Ids of the subnets for RDS


<a id="nestedatt--features--karpenter"></a>
### Nested Schema for `features.karpenter`

Required:

- `default_service_architecture` (String) The default architecture of service
- `disk_size_in_gib` (Number)
- `spot_enabled` (Boolean) Enable spot instances



<a id="nestedatt--routing_table"></a>
### Nested Schema for `routing_table`
Expand Down
17 changes: 14 additions & 3 deletions docs/resources/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ You can find complete examples within these repositories:
- `cloud_provider` (String) Cloud provider of the cluster.
- Can be: `AWS`, `GCP`, `ON_PREMISE`, `SCW`.
- `credentials_id` (String) Id of the credentials.
- `instance_type` (String) Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`
- `name` (String) Name of the cluster.
- `organization_id` (String) Id of the organization.
- `region` (String) Region of the cluster.
Expand All @@ -68,13 +67,14 @@ You can find complete examples within these repositories:
- Default: ``.
- `disk_size` (Number)
- `features` (Attributes) Features of the cluster. (see [below for nested schema](#nestedatt--features))
- `instance_type` (String) Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`, and not set for Karpenter-enabled clusters
- `kubernetes_mode` (String) Kubernetes mode of the cluster.
- Can be: `K3S`, `MANAGED`.
- Default: `MANAGED`.
- `max_running_nodes` (Number) Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters]
- `max_running_nodes` (Number) Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters; and not set for Karpenter-enabled clusters]
- Must be: `>= 1`.
- Default: `10`.
- `min_running_nodes` (Number) Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters].
- `min_running_nodes` (Number) Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters].
- Must be: `>= 1`.
- Default: `3`.
- `routing_table` (Attributes Set) List of routes of the cluster. (see [below for nested schema](#nestedatt--routing_table))
Expand All @@ -92,6 +92,7 @@ You can find complete examples within these repositories:
Optional:

- `existing_vpc` (Attributes) Network configuration if you want to install qovery on an existing VPC (see [below for nested schema](#nestedatt--features--existing_vpc))
- `karpenter` (Attributes) Karpenter parameters if you want to use Karpenter on an EKS cluster (see [below for nested schema](#nestedatt--features--karpenter))
- `static_ip` (Boolean) Static IP (AWS only) [NOTE: can't be updated after creation].
- Default: `false`.
- `vpc_subnet` (String) Custom VPC subnet (AWS only) [NOTE: can't be updated after creation].
Expand Down Expand Up @@ -120,6 +121,16 @@ Optional:
- `rds_subnets_zone_c_ids` (List of String) Ids of the subnets for RDS


<a id="nestedatt--features--karpenter"></a>
### Nested Schema for `features.karpenter`

Required:

- `default_service_architecture` (String) The default architecture of service
- `disk_size_in_gib` (Number)
- `spot_enabled` (Boolean) Enable spot instances



<a id="nestedatt--routing_table"></a>
### Nested Schema for `routing_table`
Expand Down
28 changes: 25 additions & 3 deletions qovery/data_source_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ func (r clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Computed: true,
},
"instance_type": schema.StringAttribute{
Description: "Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`",
Description: "Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`, and not set for Karpenter-enabled clusters",
Optional: true,
Computed: true,
},
"disk_size": schema.Int64Attribute{
Expand All @@ -105,7 +106,7 @@ func (r clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
},
"min_running_nodes": schema.Int64Attribute{
Description: descriptions.NewInt64MinDescription(
"Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters].",
"Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters].",
clusterMinRunningNodesMin,
&clusterMinRunningNodesDefault,
),
Expand All @@ -114,7 +115,7 @@ func (r clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
},
"max_running_nodes": schema.Int64Attribute{
Description: descriptions.NewInt64MinDescription(
"Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters]",
"Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters]",
clusterMaxRunningNodesMin,
&clusterMaxRunningNodesDefault,
),
Expand Down Expand Up @@ -226,6 +227,27 @@ func (r clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
},
},
},
"karpenter": schema.SingleNestedAttribute{
Optional: true,
Computed: false,
Description: "Karpenter parameters if you want to use Karpenter on an EKS cluster",
Attributes: map[string]schema.Attribute{
"spot_enabled": schema.BoolAttribute{
Description: "Enable spot instances",
Required: true,
Computed: false,
},
"disk_size_in_gib": schema.Int64Attribute{
Required: true,
Computed: false,
},
"default_service_architecture": schema.StringAttribute{
Description: "The default architecture of service",
Required: true,
Computed: false,
},
},
},
},
},
"routing_table": schema.SetNestedAttribute{
Expand Down
39 changes: 26 additions & 13 deletions qovery/resource_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
Expand Down Expand Up @@ -156,38 +155,31 @@ func (r clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, res
},
},
"instance_type": schema.StringAttribute{
Description: "Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`",
Required: true,
Description: "Instance type of the cluster. I.e: For Aws `t3a.xlarge`, for Scaleway `DEV-L`, and not set for Karpenter-enabled clusters",
Optional: true,
Computed: true,
},
"disk_size": schema.Int64Attribute{
Optional: true,
Computed: true,
},
"min_running_nodes": schema.Int64Attribute{
Description: descriptions.NewInt64MinDescription(
"Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters].",
"Minimum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters, and not set for Karpenter-enabled clusters].",
clusterMinRunningNodesMin,
&clusterMinRunningNodesDefault,
),
Optional: true,
Computed: true,
Default: int64default.StaticInt64(clusterMinRunningNodesDefault),
Validators: []validator.Int64{
validators.Int64MinValidator{Min: clusterMinRunningNodesMin},
},
},
"max_running_nodes": schema.Int64Attribute{
Description: descriptions.NewInt64MinDescription(
"Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters]",
"Maximum number of nodes running for the cluster. [NOTE: have to be set to 1 in case of K3S clusters; and not set for Karpenter-enabled clusters]",
clusterMaxRunningNodesMin,
&clusterMaxRunningNodesDefault,
),
Optional: true,
Computed: true,
Default: int64default.StaticInt64(clusterMaxRunningNodesDefault),
Validators: []validator.Int64{
validators.Int64MinValidator{Min: clusterMaxRunningNodesMin},
},
},
"features": schema.SingleNestedAttribute{
Description: "Features of the cluster.",
Expand Down Expand Up @@ -296,6 +288,27 @@ func (r clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, res
},
},
},
"karpenter": schema.SingleNestedAttribute{
Optional: true,
Computed: false,
Description: "Karpenter parameters if you want to use Karpenter on an EKS cluster",
Attributes: map[string]schema.Attribute{
"spot_enabled": schema.BoolAttribute{
Description: "Enable spot instances",
Required: true,
Computed: false,
},
"disk_size_in_gib": schema.Int64Attribute{
Required: true,
Computed: false,
},
"default_service_architecture": schema.StringAttribute{
Description: "The default architecture of service",
Required: true,
Computed: false,
},
},
},
},
},
"routing_table": schema.SetNestedAttribute{
Expand Down
110 changes: 109 additions & 1 deletion qovery/resource_cluster_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/pkg/errors"
"github.com/qovery/qovery-client-go"
"github.com/qovery/terraform-provider-qovery/client"
)
Expand All @@ -17,6 +18,8 @@ const (
featureIdStaticIP = "STATIC_IP"
featureIdExistingVpc = "EXISTING_VPC"
featureKeyExistingVpc = "existing_vpc"
featureIdKarpenter = "KARPENTER"
featureKeyKarpenter = "karpenter"
)

type Cluster struct {
Expand Down Expand Up @@ -107,6 +110,26 @@ func (c Cluster) toUpsertClusterRequest(state *Cluster) (*client.ClusterUpsertPa

routingTable := toClusterRouteList(c.RoutingTables)

features := toQoveryClusterFeatures(c.Features, c.KubernetesMode.String())
if features != nil {
for _, f := range features {
if f.Id != nil && *f.Id == featureIdKarpenter {
if !c.InstanceType.IsUnknown() {
return nil, errors.New("instance_type must not be defined when Karpenter feature is enabled")
}
if !c.MinRunningNodes.IsUnknown() {
return nil, errors.New("min_running_nodes must not be defined when Karpenter feature is enabled")
}
if !c.MaxRunningNodes.IsUnknown() {
return nil, errors.New("max_running_nodes must not be defined when Karpenter feature is enabled")
}
if !c.DiskSize.IsUnknown() {
return nil, errors.New("disk_size must not be defined when Karpenter feature is enabled")
}
}
}
}

var clusterCloudProviderRequest *qovery.ClusterCloudProviderInfoRequest
if state == nil || c.CredentialsId != state.CredentialsId {
clusterCloudProviderRequest = &qovery.ClusterCloudProviderInfoRequest{
Expand Down Expand Up @@ -139,7 +162,7 @@ func (c Cluster) toUpsertClusterRequest(state *Cluster) (*client.ClusterUpsertPa
DiskSize: ToInt64Pointer(c.DiskSize),
MinRunningNodes: ToInt32Pointer(c.MinRunningNodes),
MaxRunningNodes: ToInt32Pointer(c.MaxRunningNodes),
Features: toQoveryClusterFeatures(c.Features, c.KubernetesMode.String()),
Features: features,
},
ClusterRoutingTable: routingTable.toUpsertRequest(),
AdvancedSettingsJson: ToString(c.AdvancedSettingsJson),
Expand Down Expand Up @@ -251,6 +274,28 @@ func fromQoveryClusterFeatures(ff []qovery.ClusterFeatureResponse) types.Object
}
attributes[featureKeyExistingVpc] = terraformObjectValue
attributeTypes[featureKeyExistingVpc] = terraformObjectValue.Type(context.Background())
case featureIdKarpenter:
var v *qovery.ClusterFeatureKarpenterParameters = nil
if f.GetValueObject().ClusterFeatureKarpenterParametersResponse != nil {
v = &f.GetValueObject().ClusterFeatureKarpenterParametersResponse.Value
}

attrTypes := createKarpenterFeatureAttrTypes()
if v == nil {
terraformObjectValue := types.ObjectNull(attrTypes)
attributes[featureKeyKarpenter] = terraformObjectValue
attributeTypes[featureKeyKarpenter] = terraformObjectValue.Type(context.Background())
continue
}

attrVals := createKarpenterFeatureAttrValue(v)

terraformObjectValue, diagnostics := types.ObjectValue(attrTypes, attrVals)
if diagnostics.HasError() {
panic(fmt.Errorf("bad %s feature: %s", featureKeyExistingVpc, diagnostics.Errors()))
}
attributes[featureKeyKarpenter] = terraformObjectValue
attributeTypes[featureKeyKarpenter] = terraformObjectValue.Type(context.Background())
}
}

Expand All @@ -267,6 +312,15 @@ func fromQoveryClusterFeatures(ff []qovery.ClusterFeatureResponse) types.Object
attributeTypes[featureKeyStaticIP] = types.BoolType
}

// create default karpenter feature if not set yet
if attributes[featureKeyKarpenter] == nil {
attrTypes := createKarpenterFeatureAttrTypes()

terraformObjectValue := types.ObjectNull(attrTypes)
attributes[featureKeyKarpenter] = terraformObjectValue
attributeTypes[featureKeyKarpenter] = terraformObjectValue.Type(context.Background())
}

if isNull {
// Early return object null
return types.ObjectNull(attributeTypes)
Expand Down Expand Up @@ -336,5 +390,59 @@ func toQoveryClusterFeatures(f types.Object, mode string) []qovery.ClusterReques
}
}

if _, ok := f.Attributes()[featureKeyKarpenter]; ok {
v := f.Attributes()[featureKeyKarpenter].(types.Object)
if !v.IsNull() {
defaultServiceArchitecture := v.Attributes()["default_service_architecture"].(types.String).ValueString()
arch, err := toCpuArchitectureEnum(defaultServiceArchitecture)
if err != nil {
fmt.Println("Error:", err)
}

feature := qovery.ClusterFeatureKarpenterParameters{
SpotEnabled: ToBool(v.Attributes()["spot_enabled"].(types.Bool)),
DiskSizeInGib: ToInt32(v.Attributes()["disk_size_in_gib"].(types.Int64)),
DefaultServiceArchitecture: arch,
}
value := qovery.NewNullableClusterRequestFeaturesInnerValue(&qovery.ClusterRequestFeaturesInnerValue{
ClusterFeatureKarpenterParameters: &feature,
})

features = append(features, qovery.ClusterRequestFeaturesInner{
Id: StringAsPointer(featureIdKarpenter),
Value: *value,
})
}
}

return features
}

func toCpuArchitectureEnum(arch string) (qovery.CpuArchitectureEnum, error) {
switch arch {
case string(qovery.CPUARCHITECTUREENUM_AMD64), string(qovery.CPUARCHITECTUREENUM_ARM64):
return qovery.CpuArchitectureEnum(arch), nil
default:
return "", fmt.Errorf("invalid CPU architecture: %s", arch)
}
}

func createKarpenterFeatureAttrTypes() map[string]attr.Type {
attrTypes := make(map[string]attr.Type)
attrTypes["spot_enabled"] = types.BoolType
attrTypes["disk_size_in_gib"] = types.Int64Type
attrTypes["default_service_architecture"] = types.StringType

return attrTypes
}

func createKarpenterFeatureAttrValue(v *qovery.ClusterFeatureKarpenterParameters) map[string]attr.Value {
attrVals := make(map[string]attr.Value)
if v != nil {
attrVals["spot_enabled"] = FromBoolPointer(&v.SpotEnabled)
attrVals["disk_size_in_gib"] = FromInt32(v.DiskSizeInGib)
attrVals["default_service_architecture"] = FromString(string(v.DefaultServiceArchitecture))
}

return attrVals
}

0 comments on commit 250c8fb

Please sign in to comment.