/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package provider import ( "context" "fmt" "github.com/edgelesssys/constellation/v2/internal/attestation/choose" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/versions" "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" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) // Ensure provider defined types fully satisfy framework interfaces. var ( _ resource.Resource = &ClusterResource{} _ resource.ResourceWithImportState = &ClusterResource{} ) // NewClusterResource creates a new cluster resource. func NewClusterResource() resource.Resource { return &ClusterResource{} } // ClusterResource defines the resource implementation. type ClusterResource struct{} // ClusterResourceModel describes the resource data model. type ClusterResourceModel struct { UID types.String `tfsdk:"uid"` Name types.String `tfsdk:"name"` Image types.String `tfsdk:"image"` KubernetesVersion types.String `tfsdk:"kubernetes_version"` InitEndpoint types.String `tfsdk:"init_endpoint"` KubernetesAPIEndpoint types.String `tfsdk:"kubernetes_api_endpoint"` MicroserviceVersion types.String `tfsdk:"constellation_microservices_version"` ExtraMicroservices types.Object `tfsdk:"extra_microservices"` MasterSecret types.String `tfsdk:"master_secret"` InitSecret types.String `tfsdk:"init_secret"` Attestation types.Object `tfsdk:"attestation"` OwnerID types.String `tfsdk:"owner_id"` ClusterID types.String `tfsdk:"cluster_id"` Kubeconfig types.String `tfsdk:"kubeconfig"` // NetworkConfig types.Object `tfsdk:"network_config"` // TODO(elchead): do when clear what is needed } // 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{ "uid": schema.StringAttribute{ MarkdownDescription: "The UID of the cluster.", Description: "The UID of the cluster.", Required: true, }, "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.", Optional: true, // TODO(elchead): use "constell" as default }, "image": schema.StringAttribute{ MarkdownDescription: "The Constellation OS image to use in the CSP specific reference format. Use the `constellation_image` data source to find the correct image for your CSP.", Description: "The Constellation OS image to use in the CSP specific reference format. Use the `constellation_image` data source to find the correct image for your CSP. When not set, the latest default version will be used.", Optional: true, }, "kubernetes_version": schema.StringAttribute{ MarkdownDescription: fmt.Sprintf("The Kubernetes version to use for the cluster. When not set, the latest default version (%q) will be used. The supported versions are %s.", versions.Default, versions.SupportedK8sVersions()), Description: fmt.Sprintf("The Kubernetes version to use for the cluster. When not set, the latest default version (%q) will be used. The supported versions are %s.", versions.Default, versions.SupportedK8sVersions()), Optional: true, }, "constellation_microservices_version": schema.StringAttribute{ MarkdownDescription: "The Constellation microservices version to use for the cluster.", Description: "The Constellation microservices version to use for the cluster. When not set, the latest default version will be used.", Optional: true, }, "init_endpoint": schema.StringAttribute{ MarkdownDescription: "The endpoint to use for cluster initialization. This is the endpoint of the node running the bootstrapper.", Description: "The endpoint to use for cluster initialization.", Optional: true, }, "kubernetes_api_endpoint": schema.StringAttribute{ MarkdownDescription: "The endpoint to use for the Kubernetes API.", Description: "The endpoint to use for the Kubernetes API. When not set, the default endpoint will be used.", 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{ Optional: true, MarkdownDescription: "Enable the CSI driver microservice.", Description: "Enable the CSI driver microservice.", }, }, }, "master_secret": schema.StringAttribute{ MarkdownDescription: "The master secret to use for the cluster.", Description: "The master secret to use for the cluster.", Required: true, }, "init_secret": schema.StringAttribute{ MarkdownDescription: "The init secret to use for the cluster.", Description: "The init secret to use for the cluster.", Required: true, }, "attestation": newAttestationConfigAttribute(attributeInput), "owner_id": schema.StringAttribute{ MarkdownDescription: "The owner ID of the cluster.", Description: "The owner ID of the cluster.", Computed: true, }, "cluster_id": schema.StringAttribute{ MarkdownDescription: "The cluster ID of the cluster.", Description: "The cluster ID of the cluster.", Computed: true, }, "kubeconfig": schema.StringAttribute{ MarkdownDescription: "The kubeconfig of the cluster.", Description: "The kubeconfig of the cluster.", Computed: true, }, }, } } // Configure configures the resource. func (r *ClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return } // client, ok := req.ProviderData.(*http.Client) // if !ok { // resp.Diagnostics.AddError( // "Unexpected Resource Configure Type", // fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), // ) // return //} } // Create is called when the resource is created. func (r *ClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data ClusterResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } var tfAttestation attestation diags := data.Attestation.As(ctx, &tfAttestation, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } attestationVariant, err := variant.FromString(tfAttestation.Variant) if err != nil { resp.Diagnostics.AddAttributeError( path.Root("attestation_variant"), "Invalid Attestation Variant", fmt.Sprintf("Invalid attestation variant: %s", tfAttestation.Variant)) return } attestationCfg, err := convertFromTfAttestationCfg(tfAttestation, attestationVariant) if err != nil { resp.Diagnostics.AddError("Parsing attestation config", err.Error()) return } var extraMicroservices extraMicroservices diags = data.ExtraMicroservices.As(ctx, &extraMicroservices, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // TODO(elchead): implement in follow up PR data.OwnerID = types.StringValue("owner_id") data.ClusterID = types.StringValue("cluster_id") data.Kubeconfig = types.StringValue("kubeconfig") // applier := constellation.NewApplier(log) _, err = choose.Validator(attestationCfg, &tfLogger{dg: &resp.Diagnostics}) if err != nil { resp.Diagnostics.AddError("Choosing validator", err.Error()) return } // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log tflog.Trace(ctx, "created a resource") // 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) { var data ClusterResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // If applicable, this is a great opportunity to initialize any necessary // provider client data and make a call using it. // httpResp, err := r.client.Do(httpReq) // if err != nil { // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err)) // return // } // 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) { var data ClusterResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // If applicable, this is a great opportunity to initialize any necessary // provider client data and make a call using it. // httpResp, err := r.client.Do(httpReq) // if err != nil { // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err)) // return // } // 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) { var data ClusterResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // If applicable, this is a great opportunity to initialize any necessary // provider client data and make a call using it. // httpResp, err := r.client.Do(httpReq) // if err != nil { // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err)) // return // } } // ImportState imports to the resource. func (r *ClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } type tfLogger struct { dg *diag.Diagnostics } func (l *tfLogger) Infof(format string, args ...any) { tflog.Info(context.Background(), fmt.Sprintf(format, args...)) } func (l *tfLogger) Warnf(format string, args ...any) { l.dg.AddWarning(fmt.Sprintf(format, args...), "") }