2023-12-05 10:16:50 -05:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package provider
import (
2023-12-11 09:55:44 -05:00
"bytes"
2023-12-05 10:16:50 -05:00
"context"
2023-12-11 09:55:44 -05:00
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
2023-12-05 10:16:50 -05:00
"fmt"
2023-12-11 09:55:44 -05:00
"io"
"net"
2023-12-12 10:00:03 -05:00
"net/url"
2023-12-20 09:56:48 -05:00
"regexp"
"strings"
2023-12-11 09:55:44 -05:00
"time"
2023-12-05 10:16:50 -05:00
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/atls"
2023-12-05 10:16:50 -05:00
"github.com/edgelesssys/constellation/v2/internal/attestation/choose"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/cloud/azureshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
2023-12-18 04:15:54 -05:00
"github.com/edgelesssys/constellation/v2/internal/compatibility"
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/config"
2023-12-12 10:00:03 -05:00
"github.com/edgelesssys/constellation/v2/internal/constants"
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation"
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
2023-12-18 04:15:54 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd"
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
2023-12-22 04:16:36 -05:00
"github.com/edgelesssys/constellation/v2/internal/license"
2023-12-11 09:55:44 -05:00
"github.com/edgelesssys/constellation/v2/internal/semver"
2023-12-05 10:16:50 -05:00
"github.com/edgelesssys/constellation/v2/internal/versions"
2023-12-18 08:21:19 -05:00
datastruct "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data"
2023-12-20 09:56:48 -05:00
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
2023-12-05 10:16:50 -05:00
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2023-12-18 04:15:54 -05:00
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2023-12-20 09:56:48 -05:00
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2023-12-05 10:16:50 -05:00
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var (
2023-12-20 09:56:48 -05:00
// Ensure provider defined types fully satisfy framework interfaces.
_ resource . Resource = & ClusterResource { }
_ resource . ResourceWithImportState = & ClusterResource { }
_ resource . ResourceWithModifyPlan = & ClusterResource { }
_ resource . ResourceWithValidateConfig = & ClusterResource { }
cidrRegex = regexp . MustCompile ( ` ^(\d { 1,3}\.) { 3}\d { 1,3}\/\d { 1,2}$ ` )
hexRegex = regexp . MustCompile ( ` ^[0-9a-fA-F]+$ ` )
base64Regex = regexp . MustCompile ( ` ^[-A-Za-z0-9+/]*= { 0,3}$ ` )
2023-12-05 10:16:50 -05:00
)
// NewClusterResource creates a new cluster resource.
func NewClusterResource ( ) resource . Resource {
return & ClusterResource { }
}
// ClusterResource defines the resource implementation.
2023-12-11 09:55:44 -05:00
type ClusterResource struct {
2023-12-18 08:21:19 -05:00
providerData datastruct . ProviderData
newApplier func ( ctx context . Context , validator atls . Validator ) * constellation . Applier
2023-12-11 09:55:44 -05:00
}
2023-12-05 10:16:50 -05:00
// ClusterResourceModel describes the resource data model.
type ClusterResourceModel struct {
2023-12-15 04:36:58 -05:00
Name types . String ` tfsdk:"name" `
CSP types . String ` tfsdk:"csp" `
UID types . String ` tfsdk:"uid" `
2023-12-18 04:15:54 -05:00
Image types . Object ` tfsdk:"image" `
2023-12-15 04:36:58 -05:00
KubernetesVersion types . String ` tfsdk:"kubernetes_version" `
MicroserviceVersion types . String ` tfsdk:"constellation_microservice_version" `
OutOfClusterEndpoint types . String ` tfsdk:"out_of_cluster_endpoint" `
InClusterEndpoint types . String ` tfsdk:"in_cluster_endpoint" `
ExtraMicroservices types . Object ` tfsdk:"extra_microservices" `
APIServerCertSANs types . List ` tfsdk:"api_server_cert_sans" `
NetworkConfig types . Object ` tfsdk:"network_config" `
MasterSecret types . String ` tfsdk:"master_secret" `
MasterSecretSalt types . String ` tfsdk:"master_secret_salt" `
MeasurementSalt types . String ` tfsdk:"measurement_salt" `
InitSecret types . String ` tfsdk:"init_secret" `
2023-12-22 04:16:36 -05:00
LicenseID types . String ` tfsdk:"license_id" `
2023-12-15 04:36:58 -05:00
Attestation types . Object ` tfsdk:"attestation" `
GCP types . Object ` tfsdk:"gcp" `
Azure types . Object ` tfsdk:"azure" `
2023-12-11 09:55:44 -05:00
OwnerID types . String ` tfsdk:"owner_id" `
ClusterID types . String ` tfsdk:"cluster_id" `
KubeConfig types . String ` tfsdk:"kubeconfig" `
}
2023-12-18 04:15:54 -05:00
// networkConfigAttribute is the network config attribute's data model.
2024-01-04 04:00:21 -05:00
// needs basetypes because the struct might be used in ValidateConfig where these values might still be unknown. A go string type cannot handle unknown values.
2023-12-18 04:15:54 -05:00
type networkConfigAttribute struct {
2024-01-04 04:00:21 -05:00
IPCidrNode basetypes . StringValue ` tfsdk:"ip_cidr_node" `
IPCidrPod basetypes . StringValue ` tfsdk:"ip_cidr_pod" `
IPCidrService basetypes . StringValue ` tfsdk:"ip_cidr_service" `
2023-12-11 09:55:44 -05:00
}
2023-12-18 04:15:54 -05:00
// gcpAttribute is the gcp attribute's data model.
type gcpAttribute struct {
2023-12-11 09:55:44 -05:00
// ServiceAccountKey is the private key of the service account used within the cluster.
ServiceAccountKey string ` tfsdk:"service_account_key" `
ProjectID string ` tfsdk:"project_id" `
}
2023-12-18 04:15:54 -05:00
// azureAttribute is the azure attribute's data model.
type azureAttribute struct {
2023-12-11 09:55:44 -05:00
TenantID string ` tfsdk:"tenant_id" `
Location string ` tfsdk:"location" `
UamiClientID string ` tfsdk:"uami_client_id" `
UamiResourceID string ` tfsdk:"uami_resource_id" `
ResourceGroup string ` tfsdk:"resource_group" `
SubscriptionID string ` tfsdk:"subscription_id" `
NetworkSecurityGroupName string ` tfsdk:"network_security_group_name" `
LoadBalancerName string ` tfsdk:"load_balancer_name" `
2023-12-05 10:16:50 -05:00
}
2023-12-18 04:15:54 -05:00
// extraMicroservicesAttribute is the extra microservices attribute's data model.
type extraMicroservicesAttribute struct {
CSIDriver bool ` tfsdk:"csi_driver" `
}
2023-12-05 10:16:50 -05:00
// Metadata returns the metadata of the resource.
func ( r * ClusterResource ) Metadata ( _ context . Context , req resource . MetadataRequest , resp * resource . MetadataResponse ) {
resp . TypeName = req . ProviderTypeName + "_cluster"
}
// Schema returns the schema of the resource.
func ( r * ClusterResource ) Schema ( _ context . Context , _ resource . SchemaRequest , resp * resource . SchemaResponse ) {
resp . Schema = schema . Schema {
MarkdownDescription : "Resource for a Constellation cluster." ,
Description : "Resource for a Constellation cluster." ,
Attributes : map [ string ] schema . Attribute {
2023-12-11 09:55:44 -05:00
// Input attributes
"name" : schema . StringAttribute {
MarkdownDescription : "Name used in the cluster's named resources / cluster name." ,
Description : "Name used in the cluster's named resources / cluster name." ,
Required : true , // TODO: Make optional and default to Constell.
} ,
2023-12-20 09:56:48 -05:00
"csp" : newCSPAttributeSchema ( ) ,
2023-12-05 10:16:50 -05:00
"uid" : schema . StringAttribute {
MarkdownDescription : "The UID of the cluster." ,
Description : "The UID of the cluster." ,
Required : true ,
} ,
2023-12-18 04:15:54 -05:00
"image" : newImageAttributeSchema ( attributeInput ) ,
2023-12-05 10:16:50 -05:00
"kubernetes_version" : schema . StringAttribute {
2024-01-04 10:25:24 -05:00
MarkdownDescription : fmt . Sprintf ( "The Kubernetes version to use for the cluster. The supported versions are %s." , versions . SupportedK8sVersions ( ) ) ,
Description : fmt . Sprintf ( "The Kubernetes version to use for the cluster. The supported versions are %s." , versions . SupportedK8sVersions ( ) ) ,
Required : true ,
2023-12-05 10:16:50 -05:00
} ,
2023-12-11 09:55:44 -05:00
"constellation_microservice_version" : schema . StringAttribute {
2024-01-04 10:25:24 -05:00
MarkdownDescription : "The version of Constellation's microservices used within the cluster." ,
Description : "The version of Constellation's microservices used within the cluster." ,
Required : true ,
2023-12-05 10:16:50 -05:00
} ,
2023-12-11 09:55:44 -05:00
"out_of_cluster_endpoint" : schema . StringAttribute {
MarkdownDescription : "The endpoint of the cluster. Typically, this is the public IP of a loadbalancer." ,
Description : "The endpoint of the cluster. Typically, this is the public IP of a loadbalancer." ,
Required : true ,
2023-12-05 10:16:50 -05:00
} ,
2023-12-11 09:55:44 -05:00
"in_cluster_endpoint" : schema . StringAttribute {
MarkdownDescription : "The endpoint of the cluster. When not set, the out-of-cluster endpoint is used." ,
Description : "The endpoint of the cluster. When not set, the out-of-cluster endpoint is used." ,
2023-12-05 10:16:50 -05:00
Optional : true ,
} ,
"extra_microservices" : schema . SingleNestedAttribute {
MarkdownDescription : "Extra microservice settings." ,
Description : "Extra microservice settings." ,
Optional : true ,
Attributes : map [ string ] schema . Attribute {
"csi_driver" : schema . BoolAttribute {
2023-12-11 09:55:44 -05:00
MarkdownDescription : "Enable Constellation's [encrypted CSI driver](https://docs.edgeless.systems/constellation/workflows/storage)." ,
Description : "Enable Constellation's encrypted CSI driver." ,
Required : true ,
} ,
} ,
} ,
2023-12-15 04:36:58 -05:00
"api_server_cert_sans" : schema . ListAttribute {
MarkdownDescription : "List of Subject Alternative Names (SANs) for the API server certificate. Usually, this will be" +
" the out-of-cluster endpoint and the in-cluster endpoint, if existing." ,
Description : "List of Subject Alternative Names (SANs) for the API server certificate." ,
ElementType : types . StringType ,
Optional : true ,
2023-12-11 09:55:44 -05:00
} ,
"network_config" : schema . SingleNestedAttribute {
MarkdownDescription : "Configuration for the cluster's network." ,
Description : "Configuration for the cluster's network." ,
Required : true ,
Attributes : map [ string ] schema . Attribute {
"ip_cidr_node" : schema . StringAttribute {
MarkdownDescription : "CIDR range of the cluster's node network." ,
Description : "CIDR range of the cluster's node network." ,
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . RegexMatches ( cidrRegex , "Node IP CIDR must be a valid CIDR range." ) ,
} ,
2023-12-11 09:55:44 -05:00
} ,
"ip_cidr_pod" : schema . StringAttribute {
MarkdownDescription : "CIDR range of the cluster's pod network. Only required for clusters running on GCP." ,
Description : "CIDR range of the cluster's pod network. Only required for clusters running on GCP." ,
2023-12-05 10:16:50 -05:00
Optional : true ,
2023-12-11 09:55:44 -05:00
} ,
"ip_cidr_service" : schema . StringAttribute {
MarkdownDescription : "CIDR range of the cluster's service network." ,
Description : "CIDR range of the cluster's service network." ,
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . RegexMatches ( cidrRegex , "Service IP CIDR must be a valid CIDR range." ) ,
} ,
2023-12-05 10:16:50 -05:00
} ,
} ,
} ,
"master_secret" : schema . StringAttribute {
2023-12-11 09:55:44 -05:00
MarkdownDescription : "Hex-encoded 32-byte master secret for the cluster." ,
Description : "Hex-encoded 32-byte master secret for the cluster." ,
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . LengthBetween ( 64 , 64 ) ,
stringvalidator . RegexMatches ( hexRegex , "Master secret must be a hex-encoded 32-byte value." ) ,
} ,
2023-12-11 09:55:44 -05:00
} ,
"master_secret_salt" : schema . StringAttribute {
MarkdownDescription : "Hex-encoded 32-byte master secret salt for the cluster." ,
Description : "Hex-encoded 32-byte master secret salt for the cluster." ,
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . LengthBetween ( 64 , 64 ) ,
stringvalidator . RegexMatches ( hexRegex , "Master secret salt must be a hex-encoded 32-byte value." ) ,
} ,
2023-12-11 09:55:44 -05:00
} ,
"measurement_salt" : schema . StringAttribute {
MarkdownDescription : "Hex-encoded 32-byte measurement salt for the cluster." ,
Description : "Hex-encoded 32-byte measurement salt for the cluster." ,
2023-12-05 10:16:50 -05:00
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . LengthBetween ( 64 , 64 ) ,
stringvalidator . RegexMatches ( hexRegex , "Measurement salt must be a hex-encoded 32-byte value." ) ,
} ,
2023-12-05 10:16:50 -05:00
} ,
"init_secret" : schema . StringAttribute {
2023-12-11 09:55:44 -05:00
MarkdownDescription : "Secret used for initialization of the cluster." ,
Description : "Secret used for initialization of the cluster." ,
2023-12-05 10:16:50 -05:00
Required : true ,
} ,
2023-12-22 04:16:36 -05:00
"license_id" : schema . StringAttribute {
MarkdownDescription : "Constellation license ID. When not set, the community license is used." ,
Description : "Constellation license ID. When not set, the community license is used." ,
Optional : true ,
} ,
2023-12-18 04:15:54 -05:00
"attestation" : newAttestationConfigAttributeSchema ( attributeInput ) ,
2023-12-22 04:16:36 -05:00
// CSP specific inputs
2023-12-11 09:55:44 -05:00
"gcp" : schema . SingleNestedAttribute {
MarkdownDescription : "GCP-specific configuration." ,
Description : "GCP-specific configuration." ,
Optional : true ,
Attributes : map [ string ] schema . Attribute {
"service_account_key" : schema . StringAttribute {
MarkdownDescription : "Base64-encoded private key JSON object of the service account used within the cluster." ,
Description : "Base64-encoded private key JSON object of the service account used within the cluster." ,
Required : true ,
2023-12-20 09:56:48 -05:00
Validators : [ ] validator . String {
stringvalidator . RegexMatches ( base64Regex , "Service account key must be a base64-encoded JSON object." ) ,
} ,
2023-12-11 09:55:44 -05:00
} ,
"project_id" : schema . StringAttribute {
MarkdownDescription : "ID of the GCP project the cluster resides in." ,
Description : "ID of the GCP project the cluster resides in." ,
Required : true ,
} ,
} ,
} ,
"azure" : schema . SingleNestedAttribute {
MarkdownDescription : "Azure-specific configuration." ,
Description : "Azure-specific configuration." ,
Optional : true ,
Attributes : map [ string ] schema . Attribute {
"tenant_id" : schema . StringAttribute {
MarkdownDescription : "Tenant ID of the Azure account." ,
Description : "Tenant ID of the Azure account." ,
Required : true ,
} ,
"location" : schema . StringAttribute {
MarkdownDescription : "Azure Location of the cluster." ,
Description : "Azure Location of the cluster." ,
Required : true ,
} ,
"uami_client_id" : schema . StringAttribute {
MarkdownDescription : "Client ID of the User assigned managed identity (UAMI) used within the cluster." ,
Description : "Client ID of the User assigned managed identity (UAMI) used within the cluster." ,
Required : true ,
} ,
"uami_resource_id" : schema . StringAttribute {
MarkdownDescription : "Resource ID of the User assigned managed identity (UAMI) used within the cluster." ,
Description : "Resource ID of the User assigned managed identity (UAMI) used within the cluster." ,
Required : true ,
} ,
"resource_group" : schema . StringAttribute {
MarkdownDescription : "Name of the Azure resource group the cluster resides in." ,
Description : "Name of the Azure resource group the cluster resides in." ,
Required : true ,
} ,
"subscription_id" : schema . StringAttribute {
MarkdownDescription : "ID of the Azure subscription the cluster resides in." ,
Description : "ID of the Azure subscription the cluster resides in." ,
Required : true ,
} ,
"network_security_group_name" : schema . StringAttribute {
MarkdownDescription : "Name of the Azure network security group used for the cluster." ,
Description : "Name of the Azure network security group used for the cluster." ,
Required : true ,
} ,
"load_balancer_name" : schema . StringAttribute {
MarkdownDescription : "Name of the Azure load balancer used by the cluster." ,
Description : "Name of the Azure load balancer used by the cluster." ,
Required : true ,
} ,
} ,
} ,
// Computed (output) attributes
2023-12-05 10:16:50 -05:00
"owner_id" : schema . StringAttribute {
MarkdownDescription : "The owner ID of the cluster." ,
Description : "The owner ID of the cluster." ,
Computed : true ,
2023-12-18 04:15:54 -05:00
PlanModifiers : [ ] planmodifier . String {
// We know that this value will never change after creation, so we can use the state value for upgrades.
stringplanmodifier . UseStateForUnknown ( ) ,
} ,
2023-12-05 10:16:50 -05:00
} ,
"cluster_id" : schema . StringAttribute {
MarkdownDescription : "The cluster ID of the cluster." ,
Description : "The cluster ID of the cluster." ,
Computed : true ,
2023-12-18 04:15:54 -05:00
PlanModifiers : [ ] planmodifier . String {
// We know that this value will never change after creation, so we can use the state value for upgrades.
stringplanmodifier . UseStateForUnknown ( ) ,
} ,
2023-12-05 10:16:50 -05:00
} ,
"kubeconfig" : schema . StringAttribute {
MarkdownDescription : "The kubeconfig of the cluster." ,
Description : "The kubeconfig of the cluster." ,
Computed : true ,
2023-12-18 04:15:54 -05:00
Sensitive : true ,
PlanModifiers : [ ] planmodifier . String {
// We know that this value will never change after creation, so we can use the state value for upgrades.
stringplanmodifier . UseStateForUnknown ( ) ,
} ,
2023-12-05 10:16:50 -05:00
} ,
} ,
}
}
2023-12-20 09:56:48 -05:00
// ValidateConfig validates the configuration for the resource.
func ( r * ClusterResource ) ValidateConfig ( ctx context . Context , req resource . ValidateConfigRequest , resp * resource . ValidateConfigResponse ) {
var data ClusterResourceModel
resp . Diagnostics . Append ( req . Config . Get ( ctx , & data ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
// Azure Config is required for Azure
if strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . Azure . String ( ) ) && data . Azure . IsNull ( ) {
resp . Diagnostics . AddAttributeError (
path . Root ( "azure" ) ,
"Azure configuration missing" , "When csp is set to 'azure', the 'azure' configuration must be set." ,
)
}
// Azure Config should not be set for other CSPs
if ! strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . Azure . String ( ) ) && ! data . Azure . IsNull ( ) {
resp . Diagnostics . AddAttributeWarning (
path . Root ( "azure" ) ,
"Azure configuration not allowed" , "When csp is not set to 'azure', setting the 'azure' configuration has no effect." ,
)
}
// GCP Config is required for GCP
if strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . GCP . String ( ) ) && data . GCP . IsNull ( ) {
resp . Diagnostics . AddAttributeError (
path . Root ( "gcp" ) ,
"GCP configuration missing" , "When csp is set to 'gcp', the 'gcp' configuration must be set." ,
)
}
// GCP Config should not be set for other CSPs
if ! strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . GCP . String ( ) ) && ! data . GCP . IsNull ( ) {
resp . Diagnostics . AddAttributeWarning (
path . Root ( "gcp" ) ,
"GCP configuration not allowed" , "When csp is not set to 'gcp', setting the 'gcp' configuration has no effect." ,
)
}
}
2023-12-05 10:16:50 -05:00
// Configure configures the resource.
2023-12-18 08:21:19 -05:00
func ( r * ClusterResource ) Configure ( _ context . Context , req resource . ConfigureRequest , resp * resource . ConfigureResponse ) {
2023-12-05 10:16:50 -05:00
// Prevent panic if the provider has not been configured.
if req . ProviderData == nil {
return
}
2023-12-18 08:21:19 -05:00
var ok bool
r . providerData , ok = req . ProviderData . ( datastruct . ProviderData )
if ! ok {
resp . Diagnostics . AddError (
"Unexpected Resource Configure Type" ,
fmt . Sprintf ( "Expected datastruct.ProviderData, got: %T. Please report this issue to the provider developers." , req . ProviderData ) ,
)
return
}
2023-12-05 10:16:50 -05:00
2023-12-11 09:55:44 -05:00
newDialer := func ( validator atls . Validator ) * dialer . Dialer {
return dialer . New ( nil , validator , & net . Dialer { } )
}
2023-12-05 10:16:50 -05:00
2024-02-21 07:30:31 -05:00
r . newApplier = func ( ctx context . Context , _ atls . Validator ) * constellation . Applier {
2023-12-22 04:16:36 -05:00
return constellation . NewApplier ( & tfContextLogger { ctx : ctx } , & nopSpinner { } , constellation . ApplyContextTerraform , newDialer )
2023-12-11 09:55:44 -05:00
}
2023-12-05 10:16:50 -05:00
}
2023-12-18 07:55:44 -05:00
// ModifyPlan is called when the resource is planned for creation, updates, or deletion. This allows to set pre-apply
// warnings and errors.
func ( r * ClusterResource ) ModifyPlan ( ctx context . Context , req resource . ModifyPlanRequest , resp * resource . ModifyPlanResponse ) {
2023-12-22 04:16:36 -05:00
if req . Plan . Raw . IsNull ( ) {
return
}
// Read plannedState supplied by Terraform runtime into the model
var plannedState ClusterResourceModel
resp . Diagnostics . Append ( req . Plan . Get ( ctx , & plannedState ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
2024-01-11 06:04:21 -05:00
// Validate during plan. Must be done in ModifyPlan to read provider data.
// See https://developer.hashicorp.com/terraform/plugin/framework/resources/configure#define-resource-configure-method.
_ , diags := r . getMicroserviceVersion ( & plannedState )
resp . Diagnostics . Append ( diags ... )
2024-03-01 11:06:02 -05:00
var image imageAttribute
image , _ , diags = r . getImageVersion ( ctx , & plannedState )
2024-01-11 06:04:21 -05:00
resp . Diagnostics . Append ( diags ... )
if resp . Diagnostics . HasError ( ) {
return
}
2024-03-01 11:06:02 -05:00
licenseID := plannedState . LicenseID . ValueString ( )
switch {
case image . MarketplaceImage != nil && * image . MarketplaceImage :
// Marketplace images do not require a license.
case licenseID == "" :
resp . Diagnostics . AddWarning ( "Constellation license ID not set." ,
"Continuing with community license." )
case licenseID == license . CommunityLicense :
resp . Diagnostics . AddWarning ( "Using community license." ,
"For details, see https://docs.edgeless.systems/constellation/overview/license" )
}
2023-12-18 07:55:44 -05:00
// Checks running on updates to the resource. (i.e. state and plan != nil)
2023-12-22 04:16:36 -05:00
if ! req . State . Raw . IsNull ( ) {
2023-12-18 07:55:44 -05:00
// Read currentState supplied by Terraform runtime into the model
var currentState ClusterResourceModel
resp . Diagnostics . Append ( req . State . Get ( ctx , & currentState ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
// Warn the user about possibly destructive changes in case microservice changes are to be applied.
2024-01-04 10:25:24 -05:00
currVer , diags := r . getMicroserviceVersion ( & currentState )
2023-12-18 08:21:19 -05:00
resp . Diagnostics . Append ( diags ... )
if resp . Diagnostics . HasError ( ) {
return
}
2024-01-04 10:25:24 -05:00
plannedVer , diags := r . getMicroserviceVersion ( & plannedState )
2023-12-18 08:21:19 -05:00
resp . Diagnostics . Append ( diags ... )
if resp . Diagnostics . HasError ( ) {
return
}
if currVer . Compare ( plannedVer ) != 0 { // if versions are not equal
2023-12-18 07:55:44 -05:00
resp . Diagnostics . AddWarning ( "Microservice version change" ,
"Changing the microservice version can be a destructive operation.\n" +
"Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.\n" +
"It is recommended to backup the cluster's CRDs before applying this change." )
}
}
}
2023-12-05 10:16:50 -05:00
// Create is called when the resource is created.
func ( r * ClusterResource ) Create ( ctx context . Context , req resource . CreateRequest , resp * resource . CreateResponse ) {
2023-12-11 09:55:44 -05:00
// Read data supplied by Terraform runtime into the model
2023-12-05 10:16:50 -05:00
var data ClusterResourceModel
resp . Diagnostics . Append ( req . Plan . Get ( ctx , & data ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
2023-12-11 09:55:44 -05:00
// Apply changes to the cluster, including the init RPC and skipping the node upgrade.
diags := r . apply ( ctx , & data , false , true )
2023-12-05 10:16:50 -05:00
resp . Diagnostics . Append ( diags ... )
if resp . Diagnostics . HasError ( ) {
return
}
// Save data into Terraform state
resp . Diagnostics . Append ( resp . State . Set ( ctx , & data ) ... )
}
// Read is called when the resource is read or refreshed.
func ( r * ClusterResource ) Read ( ctx context . Context , req resource . ReadRequest , resp * resource . ReadResponse ) {
// Read Terraform prior state data into the model
2023-12-11 09:55:44 -05:00
var data ClusterResourceModel
2023-12-05 10:16:50 -05:00
resp . Diagnostics . Append ( req . State . Get ( ctx , & data ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
2023-12-11 09:55:44 -05:00
// All Calls to the Constellation API are idempotent, thus we don't need to implement reading.
// Alternatively, we could:
// Retrieve more up-to-date data from the cluster. e.g.:
// - CSI Driver enabled?
// - Kubernetes version?
// - Microservice version?
// - Attestation Config?
2023-12-05 10:16:50 -05:00
// Save updated data into Terraform state
resp . Diagnostics . Append ( resp . State . Set ( ctx , & data ) ... )
}
// Update is called when the resource is updated.
func ( r * ClusterResource ) Update ( ctx context . Context , req resource . UpdateRequest , resp * resource . UpdateResponse ) {
// Read Terraform plan data into the model
2023-12-11 09:55:44 -05:00
var data ClusterResourceModel
2023-12-05 10:16:50 -05:00
resp . Diagnostics . Append ( req . Plan . Get ( ctx , & data ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
2023-12-11 09:55:44 -05:00
// Apply changes to the cluster, skipping the init RPC.
diags := r . apply ( ctx , & data , true , false )
resp . Diagnostics . Append ( diags ... )
if resp . Diagnostics . HasError ( ) {
return
}
2023-12-05 10:16:50 -05:00
// Save updated data into Terraform state
resp . Diagnostics . Append ( resp . State . Set ( ctx , & data ) ... )
}
// Delete is called when the resource is destroyed.
func ( r * ClusterResource ) Delete ( ctx context . Context , req resource . DeleteRequest , resp * resource . DeleteResponse ) {
// Read Terraform prior state data into the model
2023-12-11 09:55:44 -05:00
var data ClusterResourceModel
2023-12-05 10:16:50 -05:00
resp . Diagnostics . Append ( req . State . Get ( ctx , & data ) ... )
if resp . Diagnostics . HasError ( ) {
return
}
}
// ImportState imports to the resource.
2023-12-12 10:00:03 -05:00
func ( r * ClusterResource ) ImportState ( ctx context . Context , req resource . ImportStateRequest , resp * resource . ImportStateResponse ) {
expectedSchemaMsg := fmt . Sprintf (
"Expected URI of schema '%s://?%s=<...>&%s=<...>&%s=<...>&%s=<...>'" ,
constants . ConstellationClusterURIScheme , constants . KubeConfigURIKey , constants . ClusterEndpointURIKey ,
constants . MasterSecretURIKey , constants . MasterSecretSaltURIKey )
uri , err := url . Parse ( req . ID )
if err != nil {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: %s.\n%s" , err , expectedSchemaMsg ) )
return
}
if uri . Scheme != constants . ConstellationClusterURIScheme {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Invalid scheme '%s'.\n%s" , uri . Scheme , expectedSchemaMsg ) )
return
}
// Parse query parameters
query := uri . Query ( )
kubeConfig := query . Get ( constants . KubeConfigURIKey )
clusterEndpoint := query . Get ( constants . ClusterEndpointURIKey )
masterSecret := query . Get ( constants . MasterSecretURIKey )
masterSecretSalt := query . Get ( constants . MasterSecretSaltURIKey )
if kubeConfig == "" {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Missing query parameter '%s'.\n%s" , constants . KubeConfigURIKey , expectedSchemaMsg ) )
return
}
if clusterEndpoint == "" {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Missing query parameter '%s'.\n%s" , constants . ClusterEndpointURIKey , expectedSchemaMsg ) )
return
}
if masterSecret == "" {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Missing query parameter '%s'.\n%s" , constants . MasterSecretURIKey , expectedSchemaMsg ) )
return
}
if masterSecretSalt == "" {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Missing query parameter '%s'.\n%s" , constants . MasterSecretSaltURIKey , expectedSchemaMsg ) )
return
}
decodedKubeConfig , err := base64 . StdEncoding . DecodeString ( kubeConfig )
if err != nil {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Decoding base64-encoded kubeconfig: %s." , err ) )
return
}
// Sanity checks for master secret and master secret salt
if _ , err := hex . DecodeString ( masterSecret ) ; err != nil {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Decoding hex-encoded master secret: %s." , err ) )
return
}
if _ , err := hex . DecodeString ( masterSecretSalt ) ; err != nil {
resp . Diagnostics . AddError ( "Parsing cluster URI" ,
fmt . Sprintf ( "Parsing cluster URI: Decoding hex-encoded master secret salt: %s." , err ) )
return
}
2023-12-11 09:55:44 -05:00
2023-12-12 10:00:03 -05:00
resp . Diagnostics . Append ( resp . State . SetAttribute ( ctx , path . Root ( "kubeconfig" ) , string ( decodedKubeConfig ) ) ... )
resp . Diagnostics . Append ( resp . State . SetAttribute ( ctx , path . Root ( "out_of_cluster_endpoint" ) , clusterEndpoint ) ... )
resp . Diagnostics . Append ( resp . State . SetAttribute ( ctx , path . Root ( "master_secret" ) , masterSecret ) ... )
resp . Diagnostics . Append ( resp . State . SetAttribute ( ctx , path . Root ( "master_secret_salt" ) , masterSecretSalt ) ... )
2023-12-11 09:55:44 -05:00
}
2024-01-04 04:00:21 -05:00
func ( r * ClusterResource ) validateGCPNetworkConfig ( ctx context . Context , data * ClusterResourceModel ) diag . Diagnostics {
networkCfg , diags := r . getNetworkConfig ( ctx , data )
if diags . HasError ( ) {
return diags
}
// Pod IP CIDR is required for GCP
if strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . GCP . String ( ) ) && networkCfg . IPCidrPod . ValueString ( ) == "" {
diags . AddAttributeError (
path . Root ( "network_config" ) . AtName ( "ip_cidr_pod" ) ,
"Pod IP CIDR missing" , "When csp is set to 'gcp', 'ip_cidr_pod' must be set." ,
)
}
// Pod IP CIDR should not be set for other CSPs
if ! strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . GCP . String ( ) ) && networkCfg . IPCidrPod . ValueString ( ) != "" {
diags . AddAttributeWarning (
path . Root ( "network_config" ) . AtName ( "ip_cidr_pod" ) ,
"Pod IP CIDR not allowed" , "When csp is not set to 'gcp', setting 'ip_cidr_pod' has no effect." ,
)
}
2024-01-23 03:08:23 -05:00
// Pod IP CIDR should be a valid CIDR on GCP
if strings . EqualFold ( data . CSP . ValueString ( ) , cloudprovider . GCP . String ( ) ) &&
! cidrRegex . MatchString ( networkCfg . IPCidrPod . ValueString ( ) ) {
diags . AddAttributeError (
path . Root ( "network_config" ) . AtName ( "ip_pod_cidr" ) ,
"Invalid CIDR range" , "Pod IP CIDR must be a valid CIDR range." ,
)
}
2024-01-04 04:00:21 -05:00
return diags
}
2023-12-11 09:55:44 -05:00
// apply applies changes to a cluster. It can be used for both creating and updating a cluster.
// This implements the core part of the Create and Update methods.
func ( r * ClusterResource ) apply ( ctx context . Context , data * ClusterResourceModel , skipInitRPC , skipNodeUpgrade bool ) diag . Diagnostics {
diags := diag . Diagnostics { }
// Parse and convert values from the Terraform state
// to formats the Constellation library can work with.
2024-01-04 04:00:21 -05:00
convertDiags := r . validateGCPNetworkConfig ( ctx , data )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
2023-12-11 09:55:44 -05:00
csp := cloudprovider . FromString ( data . CSP . ValueString ( ) )
// parse attestation config
att , convertDiags := r . convertAttestationConfig ( ctx , * data )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
// parse secrets (i.e. measurement salt, master secret, etc.)
secrets , convertDiags := r . convertSecrets ( * data )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
// parse API server certificate SANs
2023-12-27 11:04:35 -05:00
apiServerCertSANs , convertDiags := r . getAPIServerCertSANs ( ctx , data )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
2023-12-11 09:55:44 -05:00
}
// parse network config
2023-12-20 09:56:48 -05:00
networkCfg , getDiags := r . getNetworkConfig ( ctx , data )
diags . Append ( getDiags ... )
2023-12-11 09:55:44 -05:00
if diags . HasError ( ) {
return diags
}
// parse Constellation microservice config
2023-12-18 04:15:54 -05:00
var microserviceCfg extraMicroservicesAttribute
2023-12-11 09:55:44 -05:00
convertDiags = data . ExtraMicroservices . As ( ctx , & microserviceCfg , basetypes . ObjectAsOptions {
UnhandledNullAsEmpty : true , // we want to allow null values, as the CSIDriver field is optional
} )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
// parse Constellation microservice version
2024-01-04 10:25:24 -05:00
microserviceVersion , convertDiags := r . getMicroserviceVersion ( data )
2023-12-18 08:21:19 -05:00
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
2023-12-11 09:55:44 -05:00
return diags
}
// parse Kubernetes version
2024-01-04 10:25:24 -05:00
k8sVersion , getDiags := r . getK8sVersion ( data )
2023-12-11 09:55:44 -05:00
diags . Append ( getDiags ... )
if diags . HasError ( ) {
return diags
}
// parse OS image version
2023-12-22 04:24:13 -05:00
image , imageSemver , convertDiags := r . getImageVersion ( ctx , data )
2023-12-18 04:15:54 -05:00
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
2023-12-11 09:55:44 -05:00
2023-12-22 04:16:36 -05:00
// parse license ID
licenseID := data . LicenseID . ValueString ( )
2024-03-01 11:06:02 -05:00
switch {
case image . MarketplaceImage != nil && * image . MarketplaceImage :
licenseID = license . MarketplaceLicense
case licenseID == "" :
2023-12-22 04:16:36 -05:00
licenseID = license . CommunityLicense
}
2024-03-01 11:06:02 -05:00
2023-12-22 04:16:36 -05:00
// license ID can be base64-encoded
licenseIDFromB64 , err := base64 . StdEncoding . DecodeString ( licenseID )
if err == nil {
licenseID = string ( licenseIDFromB64 )
}
2023-12-11 09:55:44 -05:00
// Parse in-cluster service account info.
serviceAccPayload := constellation . ServiceAccountPayload { }
2023-12-18 04:15:54 -05:00
var gcpConfig gcpAttribute
var azureConfig azureAttribute
2023-12-11 09:55:44 -05:00
switch csp {
case cloudprovider . GCP :
convertDiags = data . GCP . As ( ctx , & gcpConfig , basetypes . ObjectAsOptions { } )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
decodedSaKey , err := base64 . StdEncoding . DecodeString ( gcpConfig . ServiceAccountKey )
if err != nil {
diags . AddAttributeError (
path . Root ( "gcp" ) . AtName ( "service_account_key" ) ,
"Decoding service account key" ,
fmt . Sprintf ( "Decoding base64-encoded service account key: %s" , err ) )
return diags
}
if err := json . Unmarshal ( decodedSaKey , & serviceAccPayload . GCP ) ; err != nil {
diags . AddAttributeError (
path . Root ( "gcp" ) . AtName ( "service_account_key" ) ,
"Unmarshalling service account key" ,
fmt . Sprintf ( "Unmarshalling service account key: %s" , err ) )
return diags
}
case cloudprovider . Azure :
convertDiags = data . Azure . As ( ctx , & azureConfig , basetypes . ObjectAsOptions { } )
diags . Append ( convertDiags ... )
if diags . HasError ( ) {
return diags
}
serviceAccPayload . Azure = azureshared . ApplicationCredentials {
TenantID : azureConfig . TenantID ,
Location : azureConfig . Location ,
PreferredAuthMethod : azureshared . AuthMethodUserAssignedIdentity ,
UamiResourceID : azureConfig . UamiResourceID ,
}
}
serviceAccURI , err := constellation . MarshalServiceAccountURI ( csp , serviceAccPayload )
if err != nil {
diags . AddError ( "Marshalling service account URI" , err . Error ( ) )
return diags
}
// we want to fall back to outOfClusterEndpoint if inClusterEndpoint is not set.
inClusterEndpoint := data . InClusterEndpoint . ValueString ( )
if inClusterEndpoint == "" {
inClusterEndpoint = data . OutOfClusterEndpoint . ValueString ( )
}
// setup clients
validator , err := choose . Validator ( att . config , & tfContextLogger { ctx : ctx } )
if err != nil {
diags . AddError ( "Choosing validator" , err . Error ( ) )
return diags
}
applier := r . newApplier ( ctx , validator )
// Construct in-memory state file
stateFile := state . New ( ) . SetInfrastructure ( state . Infrastructure {
UID : data . UID . ValueString ( ) ,
ClusterEndpoint : data . OutOfClusterEndpoint . ValueString ( ) ,
InClusterEndpoint : inClusterEndpoint ,
InitSecret : [ ] byte ( data . InitSecret . ValueString ( ) ) ,
APIServerCertSANs : apiServerCertSANs ,
Name : data . Name . ValueString ( ) ,
2024-01-04 04:00:21 -05:00
IPCidrNode : networkCfg . IPCidrNode . ValueString ( ) ,
2023-12-11 09:55:44 -05:00
} )
switch csp {
case cloudprovider . Azure :
stateFile . Infrastructure . Azure = & state . Azure {
ResourceGroup : azureConfig . ResourceGroup ,
SubscriptionID : azureConfig . SubscriptionID ,
NetworkSecurityGroupName : azureConfig . NetworkSecurityGroupName ,
LoadBalancerName : azureConfig . LoadBalancerName ,
UserAssignedIdentity : azureConfig . UamiClientID ,
AttestationURL : att . maaURL ,
}
case cloudprovider . GCP :
stateFile . Infrastructure . GCP = & state . GCP {
ProjectID : gcpConfig . ProjectID ,
2024-01-04 04:00:21 -05:00
IPCidrPod : networkCfg . IPCidrPod . ValueString ( ) ,
2023-12-11 09:55:44 -05:00
}
}
2023-12-22 04:16:36 -05:00
// Check license
quota , err := applier . CheckLicense ( ctx , csp , ! skipInitRPC , licenseID )
if err != nil {
diags . AddWarning ( "Unable to contact license server." , "Please keep your vCPU quota in mind." )
} else if licenseID == license . CommunityLicense {
diags . AddWarning ( "Using community license." , "For details, see https://docs.edgeless.systems/constellation/overview/license" )
} else {
tflog . Info ( ctx , fmt . Sprintf ( "Please keep your vCPU quota (%d) in mind." , quota ) )
}
2023-12-11 09:55:44 -05:00
// Now, we perform the actual applying.
// Run init RPC
var initDiags diag . Diagnostics
if ! skipInitRPC {
// run the init RPC and retrieve the post-init state
initRPCPayload := initRPCPayload {
csp : csp ,
masterSecret : secrets . masterSecret ,
measurementSalt : secrets . measurementSalt ,
apiServerCertSANs : apiServerCertSANs ,
azureCfg : azureConfig ,
gcpCfg : gcpConfig ,
networkCfg : networkCfg ,
maaURL : att . maaURL ,
k8sVersion : k8sVersion ,
inClusterEndpoint : inClusterEndpoint ,
}
initDiags = r . runInitRPC ( ctx , applier , initRPCPayload , data , validator , stateFile )
diags . Append ( initDiags ... )
if diags . HasError ( ) {
return diags
}
}
// Here, we either have the post-init values from the actual init RPC
// or, if performing an upgrade and skipping the init RPC, we have the
// values from the Terraform state.
stateFile . SetClusterValues ( state . ClusterValues {
ClusterID : data . ClusterID . ValueString ( ) ,
OwnerID : data . OwnerID . ValueString ( ) ,
MeasurementSalt : secrets . measurementSalt ,
} )
// Kubeconfig is in the state by now. Either through the init RPC or through
// already being in the state.
if err := applier . SetKubeConfig ( [ ] byte ( data . KubeConfig . ValueString ( ) ) ) ; err != nil {
diags . AddError ( "Setting kubeconfig" , err . Error ( ) )
return diags
}
// Apply attestation config
if err := applier . ApplyJoinConfig ( ctx , att . config , secrets . measurementSalt ) ; err != nil {
diags . AddError ( "Applying attestation config" , err . Error ( ) )
return diags
}
// Extend API Server Certificate SANs
if err := applier . ExtendClusterConfigCertSANs ( ctx , data . OutOfClusterEndpoint . ValueString ( ) ,
"" , apiServerCertSANs ) ; err != nil {
diags . AddError ( "Extending API server certificate SANs" , err . Error ( ) )
return diags
}
// Apply Helm Charts
payload := applyHelmChartsPayload {
csp : cloudprovider . FromString ( data . CSP . ValueString ( ) ) ,
attestationVariant : att . variant ,
k8sVersion : k8sVersion ,
microserviceVersion : microserviceVersion ,
DeployCSIDriver : microserviceCfg . CSIDriver ,
masterSecret : secrets . masterSecret ,
serviceAccURI : serviceAccURI ,
}
helmDiags := r . applyHelmCharts ( ctx , applier , payload , stateFile )
diags . Append ( helmDiags ... )
if diags . HasError ( ) {
return diags
}
if ! skipNodeUpgrade {
// Upgrade node image
err = applier . UpgradeNodeImage ( ctx ,
2023-12-18 04:15:54 -05:00
imageSemver ,
image . Reference ,
2023-12-11 09:55:44 -05:00
false )
2023-12-18 04:15:54 -05:00
var upgradeImageErr * compatibility . InvalidUpgradeError
switch {
case errors . Is ( err , kubecmd . ErrInProgress ) :
diags . AddWarning ( "Skipping OS image upgrade" , "Another upgrade is already in progress." )
case errors . As ( err , & upgradeImageErr ) :
diags . AddWarning ( "Ignoring invalid OS image upgrade" , err . Error ( ) )
case err != nil :
diags . AddError ( "Upgrading OS image" , err . Error ( ) )
2023-12-11 09:55:44 -05:00
return diags
}
2023-12-18 04:15:54 -05:00
// Upgrade Kubernetes components
err = applier . UpgradeKubernetesVersion ( ctx , k8sVersion , false )
var upgradeK8sErr * compatibility . InvalidUpgradeError
switch {
case errors . As ( err , & upgradeK8sErr ) :
diags . AddWarning ( "Ignoring invalid Kubernetes components upgrade" , err . Error ( ) )
case err != nil :
diags . AddError ( "Upgrading Kubernetes components" , err . Error ( ) )
2023-12-11 09:55:44 -05:00
return diags
}
}
return diags
2023-12-05 10:16:50 -05:00
}
2023-12-22 04:24:13 -05:00
func ( r * ClusterResource ) getImageVersion ( ctx context . Context , data * ClusterResourceModel ) ( imageAttribute , semver . Semver , diag . Diagnostics ) {
var image imageAttribute
diags := data . Image . As ( ctx , & image , basetypes . ObjectAsOptions { } )
if diags . HasError ( ) {
return imageAttribute { } , semver . Semver { } , diags
}
imageSemver , err := semver . New ( image . Version )
if err != nil {
diags . AddAttributeError (
path . Root ( "image" ) . AtName ( "version" ) ,
"Invalid image version" ,
fmt . Sprintf ( "Parsing image version (%s): %s" , image . Version , err ) )
return imageAttribute { } , semver . Semver { } , diags
}
if err := compatibility . BinaryWith ( r . providerData . Version . String ( ) , imageSemver . String ( ) ) ; err != nil {
diags . AddAttributeError (
path . Root ( "image" ) . AtName ( "version" ) ,
"Invalid image version" ,
fmt . Sprintf ( "Image version (%s) incompatible with provider version (%s): %s" , image . Version , r . providerData . Version . String ( ) , err ) )
}
return image , imageSemver , diags
}
2023-12-11 09:55:44 -05:00
// initRPCPayload groups the data required to run the init RPC.
type initRPCPayload struct {
csp cloudprovider . Provider // cloud service provider the cluster runs on.
masterSecret uri . MasterSecret // master secret of the cluster.
measurementSalt [ ] byte // measurement salt of the cluster.
apiServerCertSANs [ ] string // additional SANs to add to the API server certificate.
2023-12-18 04:15:54 -05:00
azureCfg azureAttribute // Azure-specific configuration.
gcpCfg gcpAttribute // GCP-specific configuration.
networkCfg networkConfigAttribute // network configuration of the cluster.
2023-12-11 09:55:44 -05:00
maaURL string // URL of the MAA service. Only used for Azure clusters.
k8sVersion versions . ValidK8sVersion // Kubernetes version of the cluster.
// Internal Endpoint of the cluster.
// If no internal LB is used, this should be the same as the out-of-cluster endpoint.
inClusterEndpoint string
2023-12-05 10:16:50 -05:00
}
2023-12-11 09:55:44 -05:00
// runInitRPC runs the init RPC on the cluster.
func ( r * ClusterResource ) runInitRPC ( ctx context . Context , applier * constellation . Applier , payload initRPCPayload ,
data * ClusterResourceModel , validator atls . Validator , stateFile * state . State ,
) diag . Diagnostics {
diags := diag . Diagnostics { }
clusterLogs := & bytes . Buffer { }
initOutput , err := applier . Init (
ctx , validator , stateFile , clusterLogs ,
constellation . InitPayload {
MasterSecret : payload . masterSecret ,
MeasurementSalt : payload . measurementSalt ,
K8sVersion : payload . k8sVersion ,
ConformanceMode : false , // Conformance mode does't need to be configurable through the TF provider for now.
2024-01-04 04:00:21 -05:00
ServiceCIDR : payload . networkCfg . IPCidrService . ValueString ( ) ,
2023-12-11 09:55:44 -05:00
} )
if err != nil {
var nonRetriable * constellation . NonRetriableInitError
if errors . As ( err , & nonRetriable ) {
diags . AddError ( "Cluster initialization failed." ,
fmt . Sprintf ( "This error is not recoverable. Clean up the cluster's infrastructure resources and try again.\nError: %s" , err ) )
if nonRetriable . LogCollectionErr != nil {
diags . AddError ( "Bootstrapper log collection failed." ,
fmt . Sprintf ( "Failed to collect logs from bootstrapper: %s\n" , nonRetriable . LogCollectionErr ) )
} else {
diags . AddWarning ( "Cluster log collection succeeded." , clusterLogs . String ( ) )
}
} else {
diags . AddError ( "Cluster initialization failed." , fmt . Sprintf ( "You might try to apply the resource again.\nError: %s" , err ) )
}
return diags
}
// Save data from init response into the Terraform state
data . KubeConfig = types . StringValue ( string ( initOutput . Kubeconfig ) )
data . ClusterID = types . StringValue ( initOutput . ClusterID )
data . OwnerID = types . StringValue ( initOutput . OwnerID )
return diags
2023-12-05 10:16:50 -05:00
}
2023-12-11 09:55:44 -05:00
// applyHelmChartsPayload groups the data required to apply the Helm charts.
type applyHelmChartsPayload struct {
csp cloudprovider . Provider // cloud service provider the cluster runs on.
attestationVariant variant . Variant // attestation variant used on the cluster's nodes.
k8sVersion versions . ValidK8sVersion // Kubernetes version of the cluster.
microserviceVersion semver . Semver // version of the Constellation microservices used on the cluster.
DeployCSIDriver bool // Whether to deploy the CSI driver.
masterSecret uri . MasterSecret // master secret of the cluster.
serviceAccURI string // URI of the service account used within the cluster.
2023-12-05 10:16:50 -05:00
}
2023-12-11 09:55:44 -05:00
// applyHelmCharts applies the Helm charts to the cluster.
func ( r * ClusterResource ) applyHelmCharts ( ctx context . Context , applier * constellation . Applier ,
payload applyHelmChartsPayload , state * state . State ,
) diag . Diagnostics {
diags := diag . Diagnostics { }
options := helm . Options {
CSP : payload . csp ,
AttestationVariant : payload . attestationVariant ,
K8sVersion : payload . k8sVersion ,
MicroserviceVersion : payload . microserviceVersion ,
DeployCSIDriver : payload . DeployCSIDriver ,
Force : false ,
Conformance : false , // Conformance mode does't need to be configurable through the TF provider for now.
HelmWaitMode : helm . WaitModeAtomic ,
ApplyTimeout : 10 * time . Minute ,
2023-12-18 07:55:44 -05:00
// Allow destructive changes to the cluster.
// The user has previously been warned about this when planning a microservice version change.
AllowDestructive : helm . AllowDestructive ,
2023-12-11 09:55:44 -05:00
}
executor , _ , err := applier . PrepareHelmCharts ( options , state ,
payload . serviceAccURI , payload . masterSecret , nil )
2023-12-18 04:15:54 -05:00
var upgradeErr * compatibility . InvalidUpgradeError
2023-12-11 09:55:44 -05:00
if err != nil {
2023-12-18 04:15:54 -05:00
if ! errors . As ( err , & upgradeErr ) {
diags . AddError ( "Upgrading microservices" , err . Error ( ) )
return diags
}
diags . AddWarning ( "Ignoring invalid microservice upgrade(s)" , err . Error ( ) )
2023-12-11 09:55:44 -05:00
}
if err := executor . Apply ( ctx ) ; err != nil {
diags . AddError ( "Applying Helm charts" , err . Error ( ) )
return diags
}
return diags
}
// attestationInput groups the attestation values in a state consumable by the Constellation library.
type attestationInput struct {
variant variant . Variant
maaURL string
config config . AttestationCfg
}
// convertAttestationConfig converts the attestation config from the Terraform state to the format
// used by the Constellation library.
func ( r * ClusterResource ) convertAttestationConfig ( ctx context . Context , data ClusterResourceModel ) ( attestationInput , diag . Diagnostics ) {
diags := diag . Diagnostics { }
2023-12-18 04:15:54 -05:00
var tfAttestation attestationAttribute
2023-12-11 09:55:44 -05:00
castDiags := data . Attestation . As ( ctx , & tfAttestation , basetypes . ObjectAsOptions { } )
diags . Append ( castDiags ... )
if diags . HasError ( ) {
return attestationInput { } , diags
}
attestationVariant , err := variant . FromString ( tfAttestation . Variant )
if err != nil {
diags . AddAttributeError (
path . Root ( "attestation_variant" ) ,
"Invalid Attestation Variant" ,
fmt . Sprintf ( "Invalid attestation variant: %s" , tfAttestation . Variant ) )
return attestationInput { } , diags
}
attestationCfg , err := convertFromTfAttestationCfg ( tfAttestation , attestationVariant )
if err != nil {
diags . AddAttributeError (
path . Root ( "attestation" ) ,
"Invalid Attestation Config" ,
fmt . Sprintf ( "Parsing attestation config: %s" , err ) )
return attestationInput { } , diags
}
return attestationInput { attestationVariant , tfAttestation . AzureSNPFirmwareSignerConfig . MAAURL , attestationCfg } , diags
}
// secretInput groups the secrets and salts in a state consumable by the Constellation library.
type secretInput struct {
masterSecret uri . MasterSecret
initSecret [ ] byte
measurementSalt [ ] byte
}
// convertFromTfAttestationCfg converts the secrets and salts from the Terraform state to the format
// used by the Constellation library.
func ( r * ClusterResource ) convertSecrets ( data ClusterResourceModel ) ( secretInput , diag . Diagnostics ) {
diags := diag . Diagnostics { }
masterSecret , err := hex . DecodeString ( data . MasterSecret . ValueString ( ) )
if err != nil {
diags . AddAttributeError (
path . Root ( "master_secret" ) ,
"Unmarshalling master secret" ,
fmt . Sprintf ( "Unmarshalling hex-encoded master secret: %s" , err ) )
return secretInput { } , diags
}
masterSecretSalt , err := hex . DecodeString ( data . MasterSecretSalt . ValueString ( ) )
if err != nil {
diags . AddAttributeError (
path . Root ( "master_secret_salt" ) ,
"Unmarshalling master secret salt" ,
fmt . Sprintf ( "Unmarshalling hex-encoded master secret salt: %s" , err ) )
return secretInput { } , diags
}
measurementSalt , err := hex . DecodeString ( data . MeasurementSalt . ValueString ( ) )
if err != nil {
diags . AddAttributeError (
path . Root ( "measurement_salt" ) ,
"Unmarshalling measurement salt" ,
fmt . Sprintf ( "Unmarshalling hex-encoded measurement salt: %s" , err ) )
return secretInput { } , diags
}
return secretInput {
masterSecret : uri . MasterSecret { Key : masterSecret , Salt : masterSecretSalt } ,
initSecret : [ ] byte ( data . InitSecret . ValueString ( ) ) ,
measurementSalt : measurementSalt ,
} , diags
}
// getK8sVersion returns the Kubernetes version from the Terraform state if set, and the default
// version otherwise.
2024-01-04 10:25:24 -05:00
func ( r * ClusterResource ) getK8sVersion ( data * ClusterResourceModel ) ( versions . ValidK8sVersion , diag . Diagnostics ) {
2023-12-11 09:55:44 -05:00
diags := diag . Diagnostics { }
2024-01-04 10:25:24 -05:00
k8sVersion , err := versions . NewValidK8sVersion ( data . KubernetesVersion . ValueString ( ) , true )
if err != nil {
diags . AddAttributeError (
path . Root ( "kubernetes_version" ) ,
"Invalid Kubernetes version" ,
fmt . Sprintf ( "Parsing Kubernetes version: %s" , err ) )
return "" , diags
2023-12-11 09:55:44 -05:00
}
return k8sVersion , diags
}
2023-12-18 08:21:19 -05:00
// getK8sVersion returns the Microservice version from the Terraform state if set, and the default
// version otherwise.
2024-01-04 10:25:24 -05:00
func ( r * ClusterResource ) getMicroserviceVersion ( data * ClusterResourceModel ) ( semver . Semver , diag . Diagnostics ) {
2023-12-18 08:21:19 -05:00
diags := diag . Diagnostics { }
2024-01-04 10:25:24 -05:00
ver , err := semver . New ( data . MicroserviceVersion . ValueString ( ) )
if err != nil {
diags . AddAttributeError (
path . Root ( "constellation_microservice_version" ) ,
"Invalid microservice version" ,
fmt . Sprintf ( "Parsing microservice version: %s" , err ) )
return semver . Semver { } , diags
2023-12-18 08:21:19 -05:00
}
2023-12-22 04:24:13 -05:00
if err := config . ValidateMicroserviceVersion ( r . providerData . Version , ver ) ; err != nil {
diags . AddAttributeError (
path . Root ( "constellation_microservice_version" ) ,
"Invalid microservice version" ,
fmt . Sprintf ( "Microservice version (%s) incompatible with provider version (%s): %s" , ver , r . providerData . Version , err ) )
}
2023-12-18 08:21:19 -05:00
return ver , diags
}
2023-12-20 09:56:48 -05:00
// getNetworkConfig returns the network config from the Terraform state.
func ( r * ClusterResource ) getNetworkConfig ( ctx context . Context , data * ClusterResourceModel ) ( networkConfigAttribute , diag . Diagnostics ) {
var networkCfg networkConfigAttribute
diags := data . NetworkConfig . As ( ctx , & networkCfg , basetypes . ObjectAsOptions {
UnhandledNullAsEmpty : true , // we want to allow null values, as some of the field's subfields are optional.
} )
return networkCfg , diags
}
2023-12-27 11:04:35 -05:00
func ( r * ClusterResource ) getAPIServerCertSANs ( ctx context . Context , data * ClusterResourceModel ) ( [ ] string , diag . Diagnostics ) {
if data . APIServerCertSANs . IsNull ( ) {
return nil , nil
}
apiServerCertSANs := make ( [ ] string , 0 , len ( data . APIServerCertSANs . Elements ( ) ) )
diags := data . APIServerCertSANs . ElementsAs ( ctx , & apiServerCertSANs , false )
return apiServerCertSANs , diags
}
2023-12-11 09:55:44 -05:00
// tfContextLogger is a logging adapter between the tflog package and
// Constellation's logger.
type tfContextLogger struct {
ctx context . Context // bind context to struct to satisfy interface
}
2024-02-08 09:20:01 -05:00
// Debug takes a format string and arguments as an input and logs
// them using tflog.Debug.
func ( l * tfContextLogger ) Debug ( format string , args ... any ) {
2023-12-11 09:55:44 -05:00
tflog . Debug ( l . ctx , fmt . Sprintf ( format , args ... ) )
}
2024-02-08 09:20:01 -05:00
// Info takes a format string and arguments as an input and logs
// them using tflog.Info.
func ( l * tfContextLogger ) Info ( format string , args ... any ) {
2023-12-11 09:55:44 -05:00
tflog . Info ( l . ctx , fmt . Sprintf ( format , args ... ) )
}
2024-02-08 09:20:01 -05:00
// Warn takes a format string and arguments as an input and logs
// them using tflog.Warn.
func ( l * tfContextLogger ) Warn ( format string , args ... any ) {
2023-12-11 09:55:44 -05:00
tflog . Warn ( l . ctx , fmt . Sprintf ( format , args ... ) )
}
type nopSpinner struct { io . Writer }
func ( s * nopSpinner ) Start ( string , bool ) { }
func ( s * nopSpinner ) Stop ( ) { }
func ( s * nopSpinner ) Write ( [ ] byte ) ( n int , err error ) { return 1 , nil }