From 6b46343a36baf473ce74cabace785369aae0949f Mon Sep 17 00:00:00 2001 From: Matthew Revell Date: Tue, 28 Jun 2022 07:31:48 +0100 Subject: [PATCH] Add juju_deployment read action, enable acc test --- internal/juju/deployments.go | 151 +++++++++++++++--- internal/provider/resource_deployment.go | 61 ++++++- internal/provider/resource_deployment_test.go | 9 -- 3 files changed, 183 insertions(+), 38 deletions(-) diff --git a/internal/juju/deployments.go b/internal/juju/deployments.go index 718e5bb8..3f79ac20 100644 --- a/internal/juju/deployments.go +++ b/internal/juju/deployments.go @@ -1,8 +1,10 @@ package juju import ( + "context" "errors" "fmt" + "github.com/juju/juju/rpc/params" "github.com/juju/charm/v8" jujuerrors "github.com/juju/errors" @@ -11,11 +13,14 @@ import ( apicharms "github.com/juju/juju/api/client/charms" apiclient "github.com/juju/juju/api/client/client" apimodelconfig "github.com/juju/juju/api/client/modelconfig" + "github.com/juju/juju/charmhub" "github.com/juju/juju/cmd/juju/application/utils" "github.com/juju/juju/core/constraints" "github.com/juju/juju/environs/config" "github.com/juju/juju/version" + "github.com/juju/loggo" "github.com/juju/names/v4" + "time" ) type deploymentsClient struct { @@ -37,16 +42,49 @@ type DestroyDeploymentInput struct { ModelUUID string } +type CreateDeploymentResponse struct { + AppName string + Revision int + Series string +} + +type ReadDeploymentInput struct { + ModelUUID string + AppName string +} + +type ReadDeploymentResponse struct { + Name string + Channel string + Revision int + Series string + Units int + Config map[string]interface{} +} + func newDeploymentsClient(cf ConnectionFactory) *deploymentsClient { return &deploymentsClient{ ConnectionFactory: cf, } } -func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (string, error) { +func resolveCharmURL(charmName string) (*charm.URL, error) { + path, err := charm.EnsureSchema(charmName, charm.CharmHub) + if err != nil { + return nil, err + } + charmURL, err := charm.ParseURL(path) + if err != nil { + return nil, err + } + + return charmURL, nil +} + +func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (*CreateDeploymentResponse, error) { conn, err := c.GetConnection(&input.ModelUUID) if err != nil { - return "", err + return nil, err } charmsAPIClient := apicharms.NewClient(conn) @@ -66,37 +104,33 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin appName = input.CharmName } if err := names.ValidateApplicationName(appName); err != nil { - return "", err + return nil, err } channel, err := charm.ParseChannel(input.CharmChannel) if err != nil { - return "", err + return nil, err } - path, err := charm.EnsureSchema(input.CharmName, charm.CharmHub) + charmURL, err := resolveCharmURL(input.CharmName) if err != nil { - return "", err - } - charmURL, err := charm.ParseURL(path) - if err != nil { - return "", err + return nil, err } if charmURL.Revision != UnspecifiedRevision { - return "", fmt.Errorf("cannot specify revision in a charm or bundle name") + return nil, fmt.Errorf("cannot specify revision in a charm or bundle name") } if input.CharmRevision != UnspecifiedRevision && channel.Empty() { - return "", fmt.Errorf("specifying a revision requires a channel for future upgrades") + return nil, fmt.Errorf("specifying a revision requires a channel for future upgrades") } modelConstraints, err := clientAPIClient.GetModelConstraints() if err != nil { - return "", err + return nil, err } platform, err := utils.DeducePlatform(constraints.Value{}, input.CharmSeries, modelConstraints) if err != nil { - return "", err + return nil, err } urlForOrigin := charmURL if input.CharmRevision != UnspecifiedRevision { @@ -104,7 +138,7 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin } origin, err := utils.DeduceOrigin(urlForOrigin, channel, platform) if err != nil { - return "", err + return nil, err } // Charm or bundle has been supplied as a URL so we resolve and // deploy using the store but pass in the origin command line @@ -113,15 +147,15 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin origin.Revision = &rev resolved, err := charmsAPIClient.ResolveCharms([]apicharms.CharmToResolve{{URL: charmURL, Origin: origin}}) if err != nil { - return "", err + return nil, err } if len(resolved) != 1 { - return "", fmt.Errorf("expected only one resolution, received %d", len(resolved)) + return nil, fmt.Errorf("expected only one resolution, received %d", len(resolved)) } resolvedCharm := resolved[0] if err != nil { - return "", err + return nil, err } // Figure out the actual series of the charm @@ -139,11 +173,11 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin // Get the model config attrs, err := modelconfigAPIClient.ModelGet() if err != nil { - return "", jujuerrors.Wrap(err, errors.New("cannot fetch model settings")) + return nil, jujuerrors.Wrap(err, errors.New("cannot fetch model settings")) } modelConfig, err := config.New(config.NoDefaults, attrs) if err != nil { - return "", err + return nil, err } var explicit bool @@ -162,7 +196,7 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin // Select an actually supported series series, err = charm.SeriesForCharm(series, resolvedCharm.SupportedSeries) if err != nil { - return "", err + return nil, err } // Add the charm to the model @@ -170,7 +204,7 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin charmURL = resolvedCharm.URL.WithRevision(*origin.Revision).WithArchitecture(origin.Architecture).WithSeries(series) resultOrigin, err := charmsAPIClient.AddCharm(charmURL, origin, false) if err != nil { - return "", err + return nil, err } err = applicationAPIClient.Deploy(application.DeployArgs{ @@ -182,7 +216,78 @@ func (c deploymentsClient) CreateDeployment(input *CreateDeploymentInput) (strin NumUnits: input.Units, Series: resultOrigin.Series, }) - return appName, err + return &CreateDeploymentResponse{ + AppName: appName, + Revision: *origin.Revision, + Series: series, + }, err +} + +func (c deploymentsClient) ReadDeployment(input *ReadDeploymentInput) (*ReadDeploymentResponse, error) { + conn, err := c.GetConnection(&input.ModelUUID) + if err != nil { + return nil, err + } + + applicationAPIClient := apiapplication.NewClient(conn) + defer applicationAPIClient.Close() + + charmsAPIClient := apicharms.NewClient(conn) + defer charmsAPIClient.Close() + + clientAPIClient := apiclient.NewClient(conn) + defer clientAPIClient.Close() + + apps, err := applicationAPIClient.ApplicationsInfo([]names.ApplicationTag{names.NewApplicationTag(input.AppName)}) + if err != nil { + return nil, err + } + if len(apps) > 1 { + return nil, errors.New(fmt.Sprintf("more than one result for application: %s", input.AppName)) + } + if len(apps) < 1 { + return nil, errors.New(fmt.Sprintf("no results for application: %s", input.AppName)) + } + appInfo := apps[0].Result + + status, err := clientAPIClient.Status(nil) + if err != nil { + return nil, err + } + var appStatus params.ApplicationStatus + var exists bool + if appStatus, exists = status.Applications[input.AppName]; !exists { + return nil, errors.New(fmt.Sprintf("no status returned for application: %s", input.AppName)) + } + + unitCount := len(appStatus.Units) + + chLogger := loggo.GetLogger("juju.charmhub") + config, err := charmhub.CharmHubConfig(chLogger) + if err != nil { + return nil, err + } + + client, err := charmhub.NewClient(config) + if err != nil { + return nil, err + } + + ctx, _ := context.WithTimeout(context.Background(), time.Second*5) + chInfo, err := client.Info(ctx, appInfo.Charm) + if err != nil { + return nil, err + } + + response := &ReadDeploymentResponse{ + Name: appInfo.Charm, + //Channel: appInfo.Channel, //TODO: This currently returns blank + Revision: chInfo.DefaultRelease.Revision.Revision, + Series: appInfo.Series, + Units: unitCount, + } + + return response, nil } func (c deploymentsClient) DestroyDeployment(input *DestroyDeploymentInput) error { diff --git a/internal/provider/resource_deployment.go b/internal/provider/resource_deployment.go index a745099e..11cea0bc 100644 --- a/internal/provider/resource_deployment.go +++ b/internal/provider/resource_deployment.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/juju/terraform-provider-juju/internal/juju" + "strings" ) func resourceDeployment() *schema.Resource { @@ -23,11 +24,14 @@ func resourceDeployment() *schema.Resource { Description: "A custom name for the application deployment. If empty, uses the charm's name.", Type: schema.TypeString, Optional: true, + Computed: true, + ForceNew: true, }, "model": { Description: "The name of the model where the charm is to be deployed.", Type: schema.TypeString, Required: true, + ForceNew: true, }, "charm": { Description: "The name of the charm to be installed from Charmhub.", @@ -40,6 +44,7 @@ func resourceDeployment() *schema.Resource { Description: "The name of the charm", Type: schema.TypeString, Required: true, + ForceNew: true, }, "channel": { Description: "The channel to use when deploying a charm. Specified as //.", @@ -50,14 +55,14 @@ func resourceDeployment() *schema.Resource { "revision": { Description: "The revision of the charm to deploy.", Type: schema.TypeInt, - Default: juju.UnspecifiedRevision, Optional: true, + Computed: true, }, "series": { Description: "The series on which to deploy.", Type: schema.TypeString, - Default: "", Optional: true, + Computed: true, }, }, }, @@ -94,7 +99,7 @@ func resourceDeploymentCreate(ctx context.Context, d *schema.ResourceData, meta series := charm["series"].(string) units := d.Get("units").(int) - deployedName, err := client.Deployments.CreateDeployment(&juju.CreateDeploymentInput{ + response, err := client.Deployments.CreateDeployment(&juju.CreateDeploymentInput{ ApplicationName: name, ModelUUID: modelUUID, CharmName: charmName, @@ -107,15 +112,59 @@ func resourceDeploymentCreate(ctx context.Context, d *schema.ResourceData, meta return diag.FromErr(err) } + // These values can be computed, and so set from the response. + d.Set("name", response.AppName) + + charm["revision"] = response.Revision + charm["series"] = response.Series + d.Set("charm", []map[string]interface{}{charm}) + // TODO: id generation - is there a natural ID we can use? - d.SetId(fmt.Sprintf("%s/%s", modelUUID, deployedName)) + d.SetId(fmt.Sprintf("%s/%s", modelUUID, response.AppName)) return nil } func resourceDeploymentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // TODO: Add client function to handle the appropriate JuJu API Facade Endpoint - return diag.Errorf("not implemented") + client := meta.(*juju.Client) + + id := strings.Split(d.Id(), "/") + modelUUID, appName := id[0], id[1] + + response, err := client.Deployments.ReadDeployment(&juju.ReadDeploymentInput{ + ModelUUID: modelUUID, + AppName: appName, + }) + if err != nil { + return diag.FromErr(err) + } + + if response == nil { + return nil + } + + // TODO: This is a temporary fix to preserve the defined charm channel, as we cannot currently pull this from the API + // Remove these lines and uncomment under the next TODO + charmList := d.Get("charm").([]interface{})[0].(map[string]interface{}) + charmList["name"] = response.Name + charmList["revision"] = response.Revision + charmList["series"] = response.Series + + // TODO: Once we can pull the channel from the API, remove the above and uncomment below + //charmList := []map[string]interface{}{ + // { + // "name": response.Name, + // "channel": response.Channel, + // "revision": response.Revision, + // "series": response.Series, + // }, + //} + + d.Set("name", appName) + d.Set("charm", []map[string]interface{}{charmList}) + d.Set("units", response.Units) + + return nil } func resourceDeploymentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/internal/provider/resource_deployment_test.go b/internal/provider/resource_deployment_test.go index 2557a255..7ce32bb9 100644 --- a/internal/provider/resource_deployment_test.go +++ b/internal/provider/resource_deployment_test.go @@ -7,20 +7,15 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) // TODO: test also for k8s substrate, tiny-bash charm is not supported func TestAcc_ResourceDeployment(t *testing.T) { - // TODO: remove once other operations are implemented - t.Skip("skipped until read operation is implemented") - modelName := acctest.RandomWithPrefix("tf-test-deployment") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, - CheckDestroy: testAccCheckDeploymentDestroy, Steps: []resource.TestStep{ { Config: testAccResourceDeployment(modelName), @@ -34,10 +29,6 @@ func TestAcc_ResourceDeployment(t *testing.T) { }) } -func testAccCheckDeploymentDestroy(s *terraform.State) error { - return nil -} - func testAccResourceDeployment(modelName string) string { return fmt.Sprintf(` resource "juju_model" "this" {