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

[JUJU-3442] Wait for apps before integrate #189

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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Prior to running the tests locally, ensure you have the following environmental
For example, here they are set using the currently active controller:

```shell
CONTROLLER=$(juju whoami | yq .Controller)
export CONTROLLER=$(juju whoami | yq .Controller)
export JUJU_CONTROLLER_ADDRESSES="$(juju show-controller | yq '.['$CONTROLLER']'.details.\"api-endpoints\" | tr -d "[]' "|tr -d '"'|tr -d '\n')"
export JUJU_USERNAME="$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.user|tr -d '"')"
export JUJU_PASSWORD="$(cat ~/.local/share/juju/accounts.yaml | yq .controllers.$CONTROLLER.password|tr -d '"')"
Expand Down
21 changes: 21 additions & 0 deletions internal/juju/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@
package juju

import (
"context"
"fmt"
"strings"
"time"

"github.com/juju/errors"
"github.com/juju/juju/api"
apiapplication "github.com/juju/juju/api/client/application"
apiclient "github.com/juju/juju/api/client/client"
"github.com/juju/juju/rpc/params"
)

const (
// IntegrationQueryTick defines the time to wait between ticks
// when querying the API
IntegrationApiTickWait = time.Second * 5
// IntegrationAppAvailableTimeout indicates the time to wait
// for applications to be available before integrating them
IntegrationAppAvailableTimeout = time.Second * 60
)

type integrationsClient struct {
ConnectionFactory
}
Expand All @@ -30,6 +41,7 @@ type Offer struct {

type IntegrationInput struct {
ModelUUID string
Apps []string
Endpoints []string
ViaCIDRs string
}
Expand Down Expand Up @@ -69,6 +81,15 @@ func (c integrationsClient) CreateIntegration(input *IntegrationInput) (*CreateI
client := apiapplication.NewClient(conn)
defer client.Close()

// wait for the apps to be available
ctx, cancel := context.WithTimeout(context.Background(), IntegrationAppAvailableTimeout)
defer cancel()

err = WaitForAppsAvailable(ctx, client, input.Apps, IntegrationApiTickWait)
if err != nil {
return nil, errors.New("the applications were not available to be integrated")
}

listViaCIDRs := splitCommaDelimitedList(input.ViaCIDRs)
response, err := client.AddRelation(
input.Endpoints,
Expand Down
34 changes: 32 additions & 2 deletions internal/juju/offers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package juju

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/juju/juju/api/client/application"
apiapplication "github.com/juju/juju/api/client/application"
"github.com/juju/juju/api/client/applicationoffers"
apiclient "github.com/juju/juju/api/client/client"
Expand All @@ -13,6 +16,15 @@ import (
"github.com/juju/names/v4"
)

const (
// OfferAppAvailableTimeout is the time to wait for an app to be available
// before creating an offer.
OfferAppAvailableTimeout = time.Second * 60
// OfferApiTickWait is the time to wait between consecutive requests
// to the API
OfferApiTickWait = time.Second * 5
)

type offersClient struct {
ConnectionFactory
}
Expand Down Expand Up @@ -82,6 +94,24 @@ func (c offersClient) CreateOffer(input *CreateOfferInput) (*CreateOfferResponse
offerName = input.ApplicationName
}

// connect to the corresponding model
modelConn, err := c.GetConnection(&input.ModelUUID)
if err != nil {
return nil, append(errs, err)
}
defer modelConn.Close()
applicationClient := application.NewClient(modelConn)
defer applicationClient.Close()

// wait for the app to be available
ctx, cancel := context.WithTimeout(context.Background(), OfferAppAvailableTimeout)
defer cancel()

err = WaitForAppsAvailable(ctx, applicationClient, []string{input.ApplicationName}, OfferApiTickWait)
if err != nil {
return nil, append(errs, errors.New("the application was not available to be offered"))
}

result, err := client.Offer(input.ModelUUID, input.ApplicationName, []string{input.Endpoint}, "admin", offerName, "")
if err != nil {
return nil, append(errs, err)
Expand Down Expand Up @@ -215,7 +245,7 @@ func parseModelFromURL(url string) (result string, success bool) {
return result, true
}

//This function allows the integration resource to consume the offers managed by the offer resource
// This function allows the integration resource to consume the offers managed by the offer resource
func (c offersClient) ConsumeRemoteOffer(input *ConsumeRemoteOfferInput) (*ConsumeRemoteOfferResponse, error) {
modelConn, err := c.GetConnection(&input.ModelUUID)
if err != nil {
Expand Down Expand Up @@ -282,7 +312,7 @@ func (c offersClient) ConsumeRemoteOffer(input *ConsumeRemoteOfferInput) (*Consu
return &response, nil
}

//This function allows the integration resource to destroy the offers managed by the offer resource
// This function allows the integration resource to destroy the offers managed by the offer resource
func (c offersClient) RemoveRemoteOffer(input *RemoveRemoteOfferInput) []error {
var errors []error
conn, err := c.GetConnection(&input.ModelUUID)
Expand Down
45 changes: 45 additions & 0 deletions internal/juju/utils.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package juju

import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"sync"
"time"

"encoding/json"

apiapplication "github.com/juju/juju/api/client/application"
"github.com/juju/names/v4"
"github.com/rs/zerolog/log"
)

Expand Down Expand Up @@ -109,3 +113,44 @@ func populateControllerConfig() {

log.Debug().Str("localProviderConfig", fmt.Sprintf("%#v", localProviderConfig)).Msg("local provider config was set")
}

// WaitForAppAvailable blocks the execution flow and waits until all the
// application names can be queried before the context is done. The
// tickTime param indicates the frequency used to query the API.
func WaitForAppsAvailable(ctx context.Context, client *apiapplication.Client, appsName []string, tickTime time.Duration) error {
if len(appsName) == 0 {
return nil
}
// build app tags for these apps
tags := make([]names.ApplicationTag, len(appsName))
for i, n := range appsName {
tags[i] = names.NewApplicationTag(n)
}

tick := time.NewTicker(tickTime)
for {
select {
case <-tick.C:
returned, err := client.ApplicationsInfo(tags)
// if there is no error and we get as many app infos as
// requested apps, we can assume the apps are available
if err != nil {
return err
}
totalAvailable := 0
for _, entry := range returned {
// there's no info available yet
if entry.Result == nil {
continue
}
totalAvailable++
}
// All the entries were available
if totalAvailable == len(appsName) {
return nil
}
case <-ctx.Done():
return errors.New("the context was done waiting for apps")
}
}
}
2 changes: 1 addition & 1 deletion internal/provider/resource_application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,4 @@ resource "juju_application" "subordinate" {
}
}
`, modelName, constraints)
}
}
26 changes: 15 additions & 11 deletions internal/provider/resource_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta
}

apps := d.Get("application").(*schema.Set).List()
endpoints, offerURL, err := parseEndpoints(apps)
endpoints, offerURL, appNames, err := parseEndpoints(apps)
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -101,10 +101,10 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta
if offerResponse.SAASName != "" {
endpoints = append(endpoints, offerResponse.SAASName)
}

viaCIDRs := d.Get("via").(string)
response, err := client.Integrations.CreateIntegration(&juju.IntegrationInput{
ModelUUID: modelUUID,
Apps: appNames,
Endpoints: endpoints,
ViaCIDRs: viaCIDRs,
})
Expand Down Expand Up @@ -178,11 +178,11 @@ func resourceIntegrationUpdate(ctx context.Context, d *schema.ResourceData, meta

if d.HasChange("application") {
old, new = d.GetChange("application")
oldEndpoints, oldOfferURL, err = parseEndpoints(old.(*schema.Set).List())
oldEndpoints, oldOfferURL, _, err = parseEndpoints(old.(*schema.Set).List())
if err != nil {
return diag.FromErr(err)
}
endpoints, offerURL, err = parseEndpoints(new.(*schema.Set).List())
endpoints, offerURL, _, err = parseEndpoints(new.(*schema.Set).List())
if err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -253,7 +253,7 @@ func resourceIntegrationDelete(ctx context.Context, d *schema.ResourceData, meta
}

apps := d.Get("application").(*schema.Set).List()
endpoints, offer, err := parseEndpoints(apps)
endpoints, offer, _, err := parseEndpoints(apps)
if err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -308,26 +308,26 @@ func generateID(modelName string, apps []juju.Application) string {

// This function can be used to parse the terraform data into usable juju endpoints
// it also does some sanity checks on inputs and returns user friendly errors
func parseEndpoints(apps []interface{}) (endpoints []string, offer *string, err error) {
func parseEndpoints(apps []interface{}) (endpoints []string, offer *string, appNames []string, err error) {
for _, app := range apps {
if app == nil {
return nil, nil, fmt.Errorf("you must provide a non-empty name for each application in an integration")
return nil, nil, nil, fmt.Errorf("you must provide a non-empty name for each application in an integration")
}
a := app.(map[string]interface{})
name := a["name"].(string)
offerURL := a["offer_url"].(string)
endpoint := a["endpoint"].(string)

if name == "" && offerURL == "" {
return nil, nil, fmt.Errorf("you must provide one of \"name\" or \"offer_url\"")
return nil, nil, nil, fmt.Errorf("you must provide one of \"name\" or \"offer_url\"")
}

if name != "" && offerURL != "" {
return nil, nil, fmt.Errorf("you must only provider one of \"name\" or \"offer_url\" and not both")
return nil, nil, nil, fmt.Errorf("you must only provider one of \"name\" or \"offer_url\" and not both")
}

if offerURL != "" && endpoint != "" {
return nil, nil, fmt.Errorf("\"offer_url\" cannot be provided with \"endpoint\"")
return nil, nil, nil, fmt.Errorf("\"offer_url\" cannot be provided with \"endpoint\"")
}

//Here we check if the endpoint is empty and pass just the application name, this allows juju to attempt to infer endpoints
Expand All @@ -342,9 +342,13 @@ func parseEndpoints(apps []interface{}) (endpoints []string, offer *string, err
} else {
endpoints = append(endpoints, fmt.Sprintf("%v:%v", name, endpoint))
}
// If there is no appname and this is not an offer, we have an app name
if name != "" && offerURL == "" {
appNames = append(appNames, name)
}
}

return endpoints, offer, nil
return endpoints, offer, appNames, nil
}

func parseApplications(apps []juju.Application) []map[string]interface{} {
Expand Down
Loading