diff --git a/go.mod b/go.mod index 88a9b218..52c8af5d 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( gopkg.in/httprequest.v1 v1.2.1 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 + k8s.io/client-go v0.29.0 ) require ( @@ -223,7 +224,6 @@ require ( k8s.io/api v0.29.0 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/apimachinery v0.29.0 // indirect - k8s.io/client-go v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect diff --git a/internal/juju/kubernetes_clouds.go b/internal/juju/kubernetes_clouds.go index 7ceff8d2..2e1f1df2 100644 --- a/internal/juju/kubernetes_clouds.go +++ b/internal/juju/kubernetes_clouds.go @@ -3,11 +3,24 @@ package juju +import ( + "github.com/juju/errors" + "github.com/juju/juju/api/client/cloud" + k8s "github.com/juju/juju/caas/kubernetes" + k8scloud "github.com/juju/juju/caas/kubernetes/cloud" + "k8s.io/client-go/tools/clientcmd" +) + type kubernetesCloudsClient struct { SharedClient } type CreateKubernetesCloudInput struct { + Name string + KubernetesContextName string + KubernetesConfig string + ParentCloudName string + ParentCloudRegion string } type CreateKubernetesCloudOutput struct { @@ -32,8 +45,50 @@ func newKubernetesCloudsClient(sc SharedClient) *kubernetesCloudsClient { } // CreateKubernetesCloud creates a new Kubernetes cloud with juju cloud facade. -func (c *kubernetesCloudsClient) CreateKubernetesCloud(input *CreateKubernetesCloudInput) (*CreateKubernetesCloudOutput, error) { - return nil, nil +func (c *kubernetesCloudsClient) CreateKubernetesCloud(input *CreateKubernetesCloudInput) error { + conn, err := c.GetConnection(nil) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + client := cloud.NewClient(conn) + + conf, err := clientcmd.NewClientConfigFromBytes([]byte(input.KubernetesConfig)) + if err != nil { + return errors.Annotate(err, "parsing kubernetes configuration data") + } + + apiConf, err := conf.RawConfig() + if err != nil { + return errors.Annotate(err, "fetching kubernetes configuration") + } + + var k8sContextName string + if input.KubernetesContextName == "" { + k8sContextName = apiConf.CurrentContext + } else { + k8sContextName = input.KubernetesContextName + } + + newCloud, err := k8scloud.CloudFromKubeConfigContext( + k8sContextName, + &apiConf, + k8scloud.CloudParamaters{ + Name: input.Name, + HostCloudRegion: k8s.K8sCloudOther, + }, + ) + if err != nil { + return errors.Trace(err) + } + + err = client.AddCloud(newCloud, false) + if err != nil { + return errors.Annotate(err, "adding kubernetes cloud") + } + + return nil } // ReadKubernetesCloud reads a Kubernetes cloud with juju cloud facade. diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index 92c694c1..3e4674a9 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -21,16 +21,17 @@ const ( LogDataSourceOffer = "datasource-offer" LogDataSourceSecret = "datasource-secret" - LogResourceApplication = "resource-application" - LogResourceAccessModel = "resource-assess-model" - LogResourceCredential = "resource-credential" - LogResourceMachine = "resource-machine" - LogResourceModel = "resource-model" - LogResourceOffer = "resource-offer" - LogResourceSSHKey = "resource-sshkey" - LogResourceUser = "resource-user" - LogResourceSecret = "resource-secret" - LogResourceAccessSecret = "resource-access-secret" + LogResourceApplication = "resource-application" + LogResourceAccessModel = "resource-assess-model" + LogResourceCredential = "resource-credential" + LogResourceKubernetesCloud = "resource-kubernetes-cloud" + LogResourceMachine = "resource-machine" + LogResourceModel = "resource-model" + LogResourceOffer = "resource-offer" + LogResourceSSHKey = "resource-sshkey" + LogResourceUser = "resource-user" + LogResourceSecret = "resource-secret" + LogResourceAccessSecret = "resource-access-secret" ) const LogResourceIntegration = "resource-integration" diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9dde05b9..f702f332 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -364,6 +364,7 @@ func (p *jujuProvider) Resources(_ context.Context) []func() resource.Resource { func() resource.Resource { return NewApplicationResource() }, func() resource.Resource { return NewCredentialResource() }, func() resource.Resource { return NewIntegrationResource() }, + func() resource.Resource { return NewKubernetesCloudResource() }, func() resource.Resource { return NewMachineResource() }, func() resource.Resource { return NewModelResource() }, func() resource.Resource { return NewOfferResource() }, diff --git a/internal/provider/resource_kubernetes_cloud.go b/internal/provider/resource_kubernetes_cloud.go index ebd46b57..445f52a4 100644 --- a/internal/provider/resource_kubernetes_cloud.go +++ b/internal/provider/resource_kubernetes_cloud.go @@ -5,7 +5,9 @@ package provider import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -34,12 +36,30 @@ type kubernetesCloudResource struct { type kubernetesCloudResourceModel struct { CloudName types.String `tfsdk:"name"` - KubeConfig types.String `tfsdk:"kubeconfig"` + KubernetesConfig types.String `tfsdk:"kubernetesconfig"` ParentCloudName types.String `tfsdk:"parentcloudname"` ParentCloudRegion types.String `tfsdk:"parentcloudregion"` + // ID required by the testing framework + ID types.String `tfsdk:"id"` } func (o *kubernetesCloudResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*juju.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *juju.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + o.client = client + // Create the local logging subsystem here, using the TF context when creating it. + o.subCtx = tflog.NewSubsystem(ctx, LogResourceKubernetesCloud) } func (o *kubernetesCloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -61,8 +81,8 @@ func (o *kubernetesCloudResource) Schema(_ context.Context, req resource.SchemaR stringplanmodifier.RequiresReplace(), }, }, - "kubeconfig": schema.StringAttribute{ - Description: "The kubeconfig file path for the cloud.", + "kubernetesconfig": schema.StringAttribute{ + Description: "The kubernetes config file path for the cloud.", Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -94,6 +114,31 @@ func (o *kubernetesCloudResource) Schema(_ context.Context, req resource.SchemaR // Create adds a new kubernetes cloud to controllers used now by Terraform provider. func (o *kubernetesCloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Prevent panic if the provider has not been configured. + if o.client == nil { + addClientNotConfiguredError(&resp.Diagnostics, "ssh_key", "create") + return + } + + var plan kubernetesCloudResourceModel + + // Read Terraform configuration from the request into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Create the kubernetes cloud. + err := o.client.Clouds.CreateKubernetesCloud( + &juju.CreateKubernetesCloudInput{ + Name: plan.CloudName.ValueString(), + KubernetesConfig: plan.KubernetesConfig.ValueString(), + }, + ) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create kubernetes cloud, got error %s", err)) + return + } } // Read reads the current state of the kubernetes cloud.