Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace application already exists #329

Merged
merged 7 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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