Skip to content

Commit

Permalink
Merge pull request #38 from juju/implement-juju-deployment-read
Browse files Browse the repository at this point in the history
Add juju_deployment read action
  • Loading branch information
night0wl authored Jun 30, 2022
2 parents 9bfd51b + 6b46343 commit d543fbd
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 38 deletions.
151 changes: 128 additions & 23 deletions internal/juju/deployments.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -66,45 +104,41 @@ 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 {
urlForOrigin = urlForOrigin.WithRevision(input.CharmRevision)
}
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -162,15 +196,15 @@ 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
origin = resolvedCharm.Origin.WithSeries(series)
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{
Expand All @@ -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 {
Expand Down
61 changes: 55 additions & 6 deletions internal/provider/resource_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.",
Expand All @@ -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 <track>/<risk>/<branch>.",
Expand All @@ -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,
},
},
},
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit d543fbd

Please sign in to comment.