Skip to content

Commit

Permalink
Merge pull request #427 from alesstimec/application-resource-revisions
Browse files Browse the repository at this point in the history
Adds resource revisions to applications.
  • Loading branch information
hmlanigan authored Mar 15, 2024
2 parents a8b9c58 + 69312df commit 26847d8
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 26 deletions.
11 changes: 11 additions & 0 deletions docs/resources/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ resource "juju_application" "placement_example" {
- `expose` (Block List) Makes an application publicly available over the network (see [below for nested schema](#nestedblock--expose))
- `name` (String) A custom name for the application deployment. If empty, uses the charm's name.
- `placement` (String) Specify the target location for the application's units
- `resources` (Map of Number) Charm resource revisions. Must evaluate to an integer.

There are a few scenarios that need to be considered:
* If the plan does not specify resource revision and resources are added to the plan,
resources with specified revisions will be attached to the application (equivalent
to juju attach-resource).
* If the plan does specify resource revisions and:
* If the charm revision or channel is updated, then resources get updated to the
latest revision.
* If the charm revision or channel are not updated, then no changes will take
place (juju does not have an "un-attach" command for resources).
- `trust` (Boolean) Set the trust for the application.
- `units` (Number) The number of application units to deploy for the charm.

Expand Down
74 changes: 59 additions & 15 deletions internal/juju/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type applicationsClient struct {
getApplicationAPIClient func(base.APICallCloser) ApplicationAPIClient
getClientAPIClient func(api.Connection) ClientAPIClient
getModelConfigAPIClient func(api.Connection) ModelConfigAPIClient
getResourceAPIClient func(connection api.Connection) (ResourceAPIClient, error)
}

func newApplicationClient(sc SharedClient) *applicationsClient {
Expand All @@ -80,6 +81,9 @@ func newApplicationClient(sc SharedClient) *applicationsClient {
getModelConfigAPIClient: func(conn api.Connection) ModelConfigAPIClient {
return apimodelconfig.NewClient(conn)
},
getResourceAPIClient: func(conn api.Connection) (ResourceAPIClient, error) {
return apiresources.NewClient(conn)
},
}
}

Expand Down Expand Up @@ -135,6 +139,7 @@ type CreateApplicationInput struct {
Placement string
Constraints constraints.Value
EndpointBindings map[string]string
Resources map[string]int
}

// validateAndTransform returns transformedCreateApplicationInput which
Expand All @@ -150,6 +155,7 @@ func (input CreateApplicationInput) validateAndTransform(conn api.Connection) (p
parsed.expose = input.Expose
parsed.trust = input.Trust
parsed.units = input.Units
parsed.resources = input.Resources

appName := input.ApplicationName
if appName == "" {
Expand Down Expand Up @@ -234,6 +240,7 @@ type transformedCreateApplicationInput struct {
units int
trust bool
endpointBindings map[string]string
resources map[string]int
}

type CreateApplicationResponse struct {
Expand All @@ -259,6 +266,7 @@ type ReadApplicationResponse struct {
Principal bool
Placement string
EndpointBindings map[string]string
Resources map[string]int
}

type UpdateApplicationInput struct {
Expand All @@ -277,6 +285,7 @@ type UpdateApplicationInput struct {
Placement map[string]interface{}
Constraints *constraints.Value
EndpointBindings map[string]string
Resources map[string]int
}

type DestroyApplicationInput struct {
Expand Down Expand Up @@ -336,6 +345,11 @@ func (c applicationsClient) deployFromRepository(applicationAPIClient *apiapplic
return jujuerrors.Trace(err)
}

resources := make(map[string]string)
for k, v := range transformedInput.resources {
resources[k] = strconv.Itoa(v)
}

c.Tracef("Calling DeployFromRepository")
_, _, errs := applicationAPIClient.DeployFromRepository(apiapplication.DeployFromRepositoryArg{
CharmName: transformedInput.charmName,
Expand All @@ -349,6 +363,7 @@ func (c applicationsClient) deployFromRepository(applicationAPIClient *apiapplic
Placement: transformedInput.placement,
Revision: &transformedInput.charmRevision,
Trust: transformedInput.trust,
Resources: resources,
})
return errors.Join(errs...)
}
Expand Down Expand Up @@ -476,7 +491,7 @@ func (c applicationsClient) legacyDeploy(ctx context.Context, conn api.Connectio
Origin: resultOrigin,
}

resources, err := c.processResources(charmsAPIClient, conn, charmID, transformedInput.applicationName)
resources, err := c.processResources(charmsAPIClient, conn, charmID, transformedInput.applicationName, transformedInput.resources)
if err != nil && !jujuerrors.Is(err, jujuerrors.AlreadyExists) {
return err
}
Expand Down Expand Up @@ -687,23 +702,23 @@ func splitCommaDelimitedList(list string) []string {

// processResources is a helper function to process the charm
// metadata and request the download of any additional resource.
func (c applicationsClient) processResources(charmsAPIClient *apicharms.Client, conn api.Connection, charmID apiapplication.CharmID, appName string) (map[string]string, error) {
func (c applicationsClient) processResources(charmsAPIClient *apicharms.Client, conn api.Connection, charmID apiapplication.CharmID, appName string, resources map[string]int) (map[string]string, error) {
charmInfo, err := charmsAPIClient.CharmInfo(charmID.URL.String())
if err != nil {
return nil, typedError(err)
}

// check if we have resources to request
if len(charmInfo.Meta.Resources) == 0 {
if len(charmInfo.Meta.Resources) == 0 && len(resources) == 0 {
return nil, nil
}

resourcesAPIClient, err := apiresources.NewClient(conn)
resourcesAPIClient, err := c.getResourceAPIClient(conn)
if err != nil {
return nil, err
}

return addPendingResources(appName, charmInfo.Meta.Resources, charmID, resourcesAPIClient)
return addPendingResources(appName, charmInfo.Meta.Resources, resources, charmID, resourcesAPIClient)
}

// ReadApplicationWithRetryOnNotFound calls ReadApplication until
Expand Down Expand Up @@ -929,6 +944,7 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
if err != nil {
return nil, jujuerrors.Annotate(err, "failed to get series from base")
}

defaultSpace, err := getModelDefaultSpace(modelconfigAPIClient)
if err != nil {
return nil, err
Expand All @@ -948,6 +964,21 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
}
}

resourcesAPIClient, err := c.getResourceAPIClient(conn)
if err != nil {
return nil, err
}
resources, err := resourcesAPIClient.ListResources([]string{input.AppName})
if err != nil {
return nil, jujuerrors.Annotate(err, "failed to list application resources")
}
resourceRevisions := make(map[string]int)
for _, iResources := range resources {
for _, resource := range iResources.Resources {
resourceRevisions[resource.Name] = resource.Revision
}
}

response := &ReadApplicationResponse{
Name: charmURL.Name,
Channel: appInfo.Channel,
Expand All @@ -962,6 +993,7 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
Principal: appInfo.Principal,
Placement: placement,
EndpointBindings: endpointBindings,
Resources: resourceRevisions,
}

return response, nil
Expand Down Expand Up @@ -992,7 +1024,7 @@ func (c applicationsClient) UpdateApplication(input *UpdateApplicationInput) err
clientAPIClient := c.getClientAPIClient(conn)
modelconfigAPIClient := c.getModelConfigAPIClient(conn)

resourcesAPIClient, err := apiresources.NewClient(conn)
resourcesAPIClient, err := c.getResourceAPIClient(conn)
if err != nil {
return err
}
Expand Down Expand Up @@ -1031,7 +1063,7 @@ func (c applicationsClient) UpdateApplication(input *UpdateApplicationInput) err
// before the operations with config. Because the config params
// can be changed from one revision to another. So "Revision-Config"
// ordering will help to prevent issues with the configuration parsing.
if input.Revision != nil || input.Channel != "" {
if input.Revision != nil || input.Channel != "" || len(input.Resources) != 0 {
setCharmConfig, err := c.computeSetCharmConfig(input, applicationAPIClient, charmsAPIClient, resourcesAPIClient)
if err != nil {
return err
Expand Down Expand Up @@ -1177,7 +1209,7 @@ func (c applicationsClient) computeSetCharmConfig(
input *UpdateApplicationInput,
applicationAPIClient ApplicationAPIClient,
charmsAPIClient *apicharms.Client,
resourcesAPIClient *apiresources.Client,
resourcesAPIClient ResourceAPIClient,
) (*apiapplication.SetCharmConfig, error) {
oldURL, oldOrigin, err := applicationAPIClient.GetCharmURLOrigin("", input.AppName)
if err != nil {
Expand Down Expand Up @@ -1252,7 +1284,7 @@ func (c applicationsClient) computeSetCharmConfig(
Origin: resultOrigin,
}

resourceIDs, err := c.updateResources(input.AppName, charmsAPIClient, apiCharmID, resourcesAPIClient)
resourceIDs, err := c.updateResources(input.AppName, input.Resources, charmsAPIClient, apiCharmID, resourcesAPIClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1285,12 +1317,18 @@ func strPtr(in string) *string {
return &in
}

func (c applicationsClient) updateResources(appName string, charmsAPIClient *apicharms.Client,
charmID apiapplication.CharmID, resourcesAPIClient *apiresources.Client) (map[string]string, error) {
func (c applicationsClient) updateResources(appName string, resources map[string]int, charmsAPIClient *apicharms.Client,
charmID apiapplication.CharmID, resourcesAPIClient ResourceAPIClient) (map[string]string, error) {
meta, err := utils.GetMetaResources(charmID.URL, charmsAPIClient)
if err != nil {
return nil, err
}

resourceRevisions := make(map[string]string)
for k, v := range resources {
resourceRevisions[k] = strconv.Itoa(v)
}

// TODO (cderici): Provided resources for GetUpgradeResources are user inputs.
// It's a map[string]string that should come from the plan itself. We currently
// don't have a resources block in the charm.
Expand All @@ -1299,7 +1337,7 @@ func (c applicationsClient) updateResources(appName string, charmsAPIClient *api
charmsAPIClient,
resourcesAPIClient,
appName,
nil,
resourceRevisions, // nil
meta,
)
if err != nil {
Expand All @@ -1309,18 +1347,24 @@ func (c applicationsClient) updateResources(appName string, charmsAPIClient *api
return nil, nil
}

return addPendingResources(appName, filtered, charmID, resourcesAPIClient)
return addPendingResources(appName, filtered, resources, charmID, resourcesAPIClient)
}

func addPendingResources(appName string, resourcesToBeAdded map[string]charmresources.Meta,
charmID apiapplication.CharmID, resourcesAPIClient *apiresources.Client) (map[string]string, error) {
func addPendingResources(appName string, resourcesToBeAdded map[string]charmresources.Meta, resourceRevisions map[string]int,
charmID apiapplication.CharmID, resourcesAPIClient ResourceAPIClient) (map[string]string, error) {
pendingResources := []charmresources.Resource{}
for _, v := range resourcesToBeAdded {
aux := charmresources.Resource{
Meta: v,
Origin: charmresources.OriginStore,
Revision: -1,
}
if resourceRevisions != nil {
if revision, ok := resourceRevisions[v.Name]; ok {
aux.Revision = revision
}
}

pendingResources = append(pendingResources, aux)
}

Expand Down
13 changes: 12 additions & 1 deletion internal/juju/applications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/juju/juju/api/base"
"github.com/juju/juju/core/constraints"
"github.com/juju/juju/core/model"
"github.com/juju/juju/core/resources"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/rpc/params"
"github.com/juju/names/v4"
Expand All @@ -31,7 +32,7 @@ type ApplicationSuite struct {

mockApplicationClient *MockApplicationAPIClient
mockClient *MockClientAPIClient

mockResourceAPIClient *MockResourceAPIClient
mockConnection *MockConnection
mockModelConfigClient *MockModelConfigAPIClient
mockSharedClient *MockSharedClient
Expand All @@ -49,6 +50,13 @@ func (s *ApplicationSuite) setupMocks(t *testing.T) *gomock.Controller {
s.mockConnection = NewMockConnection(ctlr)
s.mockConnection.EXPECT().Close().Return(nil).AnyTimes()

s.mockResourceAPIClient = NewMockResourceAPIClient(ctlr)
s.mockResourceAPIClient.EXPECT().ListResources(gomock.Any()).DoAndReturn(
func(applications []string) ([]resources.ApplicationResources, error) {
results := make([]resources.ApplicationResources, len(applications))
return results, nil
}).AnyTimes()

s.mockModelConfigClient = NewMockModelConfigAPIClient(ctlr)
minConfig := map[string]interface{}{
"name": "test",
Expand Down Expand Up @@ -90,6 +98,9 @@ func (s *ApplicationSuite) getApplicationsClient() applicationsClient {
getModelConfigAPIClient: func(_ api.Connection) ModelConfigAPIClient {
return s.mockModelConfigClient
},
getResourceAPIClient: func(_ api.Connection) (ResourceAPIClient, error) {
return s.mockResourceAPIClient, nil
},
}
}

Expand Down
7 changes: 7 additions & 0 deletions internal/juju/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"github.com/juju/juju/api"
apiapplication "github.com/juju/juju/api/client/application"
apiclient "github.com/juju/juju/api/client/client"
apiresources "github.com/juju/juju/api/client/resources"
apicommoncharm "github.com/juju/juju/api/common/charm"
"github.com/juju/juju/core/constraints"
"github.com/juju/juju/core/model"
"github.com/juju/juju/core/resources"
"github.com/juju/juju/rpc/params"
"github.com/juju/names/v4"
)
Expand Down Expand Up @@ -55,3 +57,8 @@ type ApplicationAPIClient interface {
type ModelConfigAPIClient interface {
ModelGet() (map[string]interface{}, error)
}

type ResourceAPIClient interface {
AddPendingResources(args apiresources.AddPendingResourcesArgs) ([]string, error)
ListResources(applications []string) ([]resources.ApplicationResources, error)
}
Loading

0 comments on commit 26847d8

Please sign in to comment.