Skip to content

Commit

Permalink
Merge pull request #329 from hmlanigan/replace-application-already-ex…
Browse files Browse the repository at this point in the history
…ists

Replace application already exists
  • Loading branch information
hmlanigan authored Oct 26, 2023
2 parents d7d4e11 + 1c4a657 commit 7172108
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 126 deletions.
19 changes: 19 additions & 0 deletions docs/resources/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ resource "juju_integration" "this" {
name = juju_application.percona-cluster.name
endpoint = "server"
}
# Add any RequiresReplace schema attributes of
# an application in this integration to ensure
# it is recreated if one of the applications
# is Destroyed and Recreated by terraform. E.G.:
lifecycle {
replace_triggered_by = [
juju_application.wordpress.name,
juju_application.wordpress.model,
juju_application.wordpress.constraints,
juju_application.wordpress.placement,
juju_application.wordpress.charm.name,
juju_application.percona-cluster.name,
juju_application.percona-cluster.model,
juju_application.percona-cluster.constraints,
juju_application.percona-cluster.placement,
juju_application.percona-cluster.charm.name,
]
}
}
```

Expand Down
19 changes: 19 additions & 0 deletions examples/resources/juju_integration/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,23 @@ resource "juju_integration" "this" {
name = juju_application.percona-cluster.name
endpoint = "server"
}

# Add any RequiresReplace schema attributes of
# an application in this integration to ensure
# it is recreated if one of the applications
# is Destroyed and Recreated by terraform. E.G.:
lifecycle {
replace_triggered_by = [
juju_application.wordpress.name,
juju_application.wordpress.model,
juju_application.wordpress.constraints,
juju_application.wordpress.placement,
juju_application.wordpress.charm.name,
juju_application.percona-cluster.name,
juju_application.percona-cluster.model,
juju_application.percona-cluster.constraints,
juju_application.percona-cluster.placement,
juju_application.percona-cluster.charm.name,
]
}
}
132 changes: 87 additions & 45 deletions internal/juju/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func resolveCharmURL(charmName string) (*charm.URL, error) {
return charmURL, nil
}

func (c applicationsClient) CreateApplication(input *CreateApplicationInput) (*CreateApplicationResponse, error) {
func (c applicationsClient) CreateApplication(ctx context.Context, input *CreateApplicationInput) (*CreateApplicationResponse, error) {
appName := input.ApplicationName
if appName == "" {
appName = input.CharmName
Expand Down Expand Up @@ -276,25 +276,6 @@ func (c applicationsClient) CreateApplication(input *CreateApplicationInput) (*C
userBase, suggestedBase)
}

// Add the charm to the model
origin = resolvedOrigin.WithSeries(seriesToUse)
charmURL = resolvedURL.WithSeries(seriesToUse)

resultOrigin, err := charmsAPIClient.AddCharm(charmURL, origin, false)
if err != nil {
return nil, err
}

charmID := apiapplication.CharmID{
URL: charmURL,
Origin: resultOrigin,
}

resources, err := c.processResources(charmsAPIClient, conn, charmID, appName)
if err != nil {
return nil, err
}

appConfig := input.Config
if appConfig == nil {
appConfig = make(map[string]string)
Expand All @@ -318,24 +299,84 @@ func (c applicationsClient) CreateApplication(input *CreateApplicationInput) (*C
}
}

args := apiapplication.DeployArgs{
CharmID: charmID,
ApplicationName: appName,
NumUnits: input.Units,
// Still supply series, to be compatible with juju 2.9 controllers.
// 3.x controllers will only use the CharmOrigin and its base.
Series: resultOrigin.Series,
CharmOrigin: resultOrigin,
Config: appConfig,
Cons: input.Constraints,
Resources: resources,
Placement: placements,
}
c.Tracef("Calling Deploy", map[string]interface{}{"args": args})
err = applicationAPIClient.Deploy(args)
// Add the charm to the model
origin = resolvedOrigin.WithSeries(seriesToUse)
charmURL = resolvedURL.WithSeries(seriesToUse)

// If a plan element, with RequiresReplace in the schema, is
// changed. Terraform calls the Destroy method then the Create
// method for resource. This provider does not wait for Destroy
// to be complete before returning. Therefore, a race may occur
// of tearing down and reading the same charm.
//
// Do the actual work to create an application within Retry.
// Errors seen so far include:
// * cannot add application "replace": charm "ch:amd64/jammy/mysql-196" not found
// * cannot add application "replace": application already exists
// * cannot add application "replace": charm: not found or not alive
err = retry.Call(retry.CallArgs{
Func: func() error {
resultOrigin, err := charmsAPIClient.AddCharm(charmURL, origin, false)
if err != nil {
err2 := typedError(err)
// If the charm is AlreadyExists, keep going, we
// may still be able to create the application. It's
// also possible we have multiple applications using
// the same charm.
if !jujuerrors.Is(err2, jujuerrors.AlreadyExists) {
return err2
}
}

charmID := apiapplication.CharmID{
URL: charmURL,
Origin: resultOrigin,
}

resources, err := c.processResources(charmsAPIClient, conn, charmID, appName)
if err != nil && !jujuerrors.Is(err, jujuerrors.AlreadyExists) {
return err
}

args := apiapplication.DeployArgs{
CharmID: charmID,
ApplicationName: appName,
NumUnits: input.Units,
// Still supply series, to be compatible with juju 2.9 controllers.
// 3.x controllers will only use the CharmOrigin and its base.
Series: resultOrigin.Series,
CharmOrigin: resultOrigin,
Config: appConfig,
Cons: input.Constraints,
Resources: resources,
Placement: placements,
}
c.Tracef("Calling Deploy", map[string]interface{}{"args": args})
if err = applicationAPIClient.Deploy(args); err != nil {
return typedError(err)
}
return nil
},
IsFatalError: func(err error) bool {
// If we hit AlreadyExists, it is from Deploy only under 2
// scenarios:
// 1. User error, the application has already been created?
// 2. We're replacing the application and tear down hasn't
// finished yet, we should try again.
return !errors.Is(err, jujuerrors.NotFound) && !errors.Is(err, jujuerrors.AlreadyExists)
},
NotifyFunc: func(err error, attempt int) {
c.Errorf(err, fmt.Sprintf("deploy application %q retry", appName))
message := fmt.Sprintf("waiting for application %q deploy, attempt %d", appName, attempt)
c.Debugf(message)
},
BackoffFunc: retry.DoubleDelay,
Attempts: 30,
Delay: time.Second,
Clock: clock.WallClock,
Stop: ctx.Done(),
})
if err != nil {
// unfortunate error during deployment
return nil, err
}

Expand Down Expand Up @@ -516,7 +557,7 @@ func splitCommaDelimitedList(list string) []string {
func (c applicationsClient) processResources(charmsAPIClient *apicharms.Client, conn api.Connection, charmID apiapplication.CharmID, appName string) (map[string]string, error) {
charmInfo, err := charmsAPIClient.CharmInfo(charmID.URL.String())
if err != nil {
return nil, err
return nil, typedError(err)
}

// check if we have resources to request
Expand Down Expand Up @@ -615,16 +656,17 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
return nil, fmt.Errorf("no status returned for application: %s", input.AppName)
}

allocatedMachines := make([]string, 0)
placementCount := 0
allocatedMachines := set.NewStrings()
for _, v := range appStatus.Units {
allocatedMachines = append(allocatedMachines, v.Machine)
placementCount += 1
if v.Machine != "" {
allocatedMachines.Add(v.Machine)
}
}
// sort the list
sort.Strings(allocatedMachines)

placement := strings.Join(allocatedMachines, ",")
var placement string
if !allocatedMachines.IsEmpty() {
placement = strings.Join(allocatedMachines.SortedValues(), ",")
}

unitCount := len(appStatus.Units)
// if we have a CAAS we use scale instead of units length
Expand Down Expand Up @@ -1060,7 +1102,7 @@ func addPendingResources(appName string, resourcesToBeAdded map[string]charmreso

toRequest, err := resourcesAPIClient.AddPendingResources(resourcesReq)
if err != nil {
return nil, err
return nil, typedError(err)
}

// now build a map with the resource name and the corresponding UUID
Expand Down
29 changes: 29 additions & 0 deletions internal/juju/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2023 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package juju

import (
"strings"

jujuerrors "github.com/juju/errors"
)

func typedError(err error) error {
switch {
case strings.Contains(err.Error(), "not found"):
return jujuerrors.WithType(err, jujuerrors.NotFound)
case strings.Contains(err.Error(), "already exists"):
return jujuerrors.WithType(err, jujuerrors.AlreadyExists)
case strings.Contains(err.Error(), "user not valid"):
return jujuerrors.WithType(err, jujuerrors.UserNotFound)
case strings.Contains(err.Error(), "not valid"):
return jujuerrors.WithType(err, jujuerrors.NotValid)
case strings.Contains(err.Error(), "not implemented"):
return jujuerrors.WithType(err, jujuerrors.NotImplemented)
case strings.Contains(err.Error(), "not yet available"):
return jujuerrors.WithType(err, jujuerrors.NotYetAvailable)
default:
return err
}
}
32 changes: 17 additions & 15 deletions internal/provider/resource_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,21 +385,23 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq
}

modelName := plan.ModelName.ValueString()
createResp, err := r.client.Applications.CreateApplication(&juju.CreateApplicationInput{
ApplicationName: plan.ApplicationName.ValueString(),
ModelName: modelName,
CharmName: charmName,
CharmChannel: channel,
CharmRevision: revision,
CharmBase: planCharm.Base.ValueString(),
CharmSeries: planCharm.Series.ValueString(),
Units: int(plan.UnitCount.ValueInt64()),
Config: configField,
Constraints: parsedConstraints,
Trust: plan.Trust.ValueBool(),
Expose: expose,
Placement: plan.Placement.ValueString(),
})
createResp, err := r.client.Applications.CreateApplication(ctx,
&juju.CreateApplicationInput{
ApplicationName: plan.ApplicationName.ValueString(),
ModelName: modelName,
CharmName: charmName,
CharmChannel: channel,
CharmRevision: revision,
CharmBase: planCharm.Base.ValueString(),
CharmSeries: planCharm.Series.ValueString(),
Units: int(plan.UnitCount.ValueInt64()),
Config: configField,
Constraints: parsedConstraints,
Trust: plan.Trust.ValueBool(),
Expose: expose,
Placement: plan.Placement.ValueString(),
},
)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create application, got error: %s", err))
return
Expand Down
Loading

0 comments on commit 7172108

Please sign in to comment.