Skip to content

Commit

Permalink
feat(application) update CRUD function for application resource
Browse files Browse the repository at this point in the history
This commit includes several updates to the resource CRUD functions in the Juju provider. The changes include:

- Added error handling for application not found in `handleApplicationNotFoundError` function.
- Added storage conintes to Create and Read functions
- Modifier retry process to read application changes, to be able to wait
  for storage creation
  • Loading branch information
anvial committed Jun 6, 2024
1 parent aa0ccbd commit 4c1b081
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 40 deletions.
15 changes: 15 additions & 0 deletions docs/resources/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ resource "juju_application" "placement_example" {
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).
- `storage` (Attributes Set) Configure storage constraints for the juju application. (see [below for nested schema](#nestedatt--storage))
- `trust` (Boolean) Set the trust for the application.
- `units` (Number) The number of application units to deploy for the charm.

Expand Down Expand Up @@ -122,6 +123,20 @@ Optional:
- `endpoints` (String) Expose only the ports that charms have opened for this comma-delimited list of endpoints
- `spaces` (String) A comma-delimited list of spaces that should be able to access the application ports once exposed.


<a id="nestedatt--storage"></a>
### Nested Schema for `storage`

Required:

- `label` (String) The specific storage option defined in the charm.
- `size` (String) The size of each volume. E.g. 100G

Optional:

- `count` (Number) The number of volumes.
- `pool` (String) Name the storage pool to use. E.g. ebs on aws.

## Import

Import is supported using the following syntax:
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ resource "juju_machine" "this_machine" {

- `base` (String) The operating system to install on the new machine(s). E.g. [email protected].
- `constraints` (String) Machine constraints that overwrite those available from 'juju get-model-constraints' and provider's defaults.
- `disks` (String) Storage constraints for disks to attach to the machine(s).
- `disks` (String) StorageContraints constraints for disks to attach to the machine(s).
- `name` (String) A name for the machine resource in Terraform.
- `placement` (String) Additional information about how to allocate the machine in the cloud.
- `private_key_file` (String) The file path to read the private key from.
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
)

require (
github.com/dustin/go-humanize v1.0.1
github.com/hashicorp/terraform-plugin-framework v1.7.0
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.23.0
Expand All @@ -26,6 +27,7 @@ require (
github.com/juju/names/v5 v5.0.0
github.com/juju/retry v1.0.0
github.com/juju/utils/v3 v3.1.1
github.com/juju/utils/v4 v4.0.2
github.com/juju/version/v2 v2.0.1
github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.9.0
Expand Down Expand Up @@ -70,7 +72,6 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
Expand Down Expand Up @@ -125,6 +126,7 @@ require (
github.com/juju/idmclient/v2 v2.0.0 // indirect
github.com/juju/jsonschema v1.0.0 // indirect
github.com/juju/loggo v1.0.0 // indirect
github.com/juju/loggo/v2 v2.0.0 // indirect
github.com/juju/lru v1.0.0 // indirect
github.com/juju/lumberjack/v2 v2.0.2 // indirect
github.com/juju/mgo/v3 v3.0.4 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ github.com/juju/juju v0.0.0-20240415234708-a7538882134d h1:yK8L5+7fJvzsNkjUkkEps
github.com/juju/juju v0.0.0-20240415234708-a7538882134d/go.mod h1:8W1iXQ/tGftslRzNF/1U8mVZFjtqc0zZKygwuIcmbPI=
github.com/juju/loggo v1.0.0 h1:Y6ZMQOGR9Aj3BGkiWx7HBbIx6zNwNkxhVNOHU2i1bl0=
github.com/juju/loggo v1.0.0/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg=
github.com/juju/loggo/v2 v2.0.0 h1:PzyVIn+NgoZ22QUtPgKF/lh+6SnaCOEXhcP+sE4FhOk=
github.com/juju/loggo/v2 v2.0.0/go.mod h1:647d6WvXBLj5lvka2qBvccr7vMIvF2KFkEH+0ZuFOUM=
github.com/juju/lru v1.0.0 h1:FP8mBNF3jBnKwGO5PtsR+8iIegx8DREfhRhpcGpYcn4=
github.com/juju/lru v1.0.0/go.mod h1:YauKGp6PAhOQyGuTOkiFdXVP1jR3vLkp/FI71GcpYcQ=
github.com/juju/lumberjack/v2 v2.0.2 h1:FlxrR62Vb7FfN7jwpSBqqereyq5bBQUF0LqOhZ2VGeI=
Expand Down Expand Up @@ -402,14 +404,16 @@ github.com/juju/rpcreflect v1.2.0/go.mod h1:yWSyMkWOwQGqUVxvboHn1c3CuCQrQ5LgV//8
github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI=
github.com/juju/schema v1.2.0 h1:+XywM0pYzuhGebQiK1aR4Bj7Q7nLV5MihcOgq6dLLxs=
github.com/juju/schema v1.2.0/go.mod h1:VdljuJLc45loM79LYm4yKKmPJwK1bPKRekvMVlfywU0=
github.com/juju/testing v1.1.0 h1:+WWez0vCu6dtnpLIzfuuo3bN3x62LBIyMDCfvMYP+Qg=
github.com/juju/testing v1.1.0/go.mod h1:1XQGptw6JWFvRWb3ewilUdTBG0oGcoI2kdX9Z1VEzhU=
github.com/juju/testing v1.2.0 h1:Q0wxjaxx4XPVEN+SgzxKr3d82pjmSBcuM3WndAU391c=
github.com/juju/testing v1.2.0/go.mod h1:lqZVzNwBKAbylGZidK77ts6kIdoOkmD52+4m0ysetPo=
github.com/juju/txn/v3 v3.0.2 h1:ibKhRlQmslPj88QfxND8hX4Sv6rTRKOb+HSp/ti1sEg=
github.com/juju/txn/v3 v3.0.2/go.mod h1:DlFlqNZkgzE4NolIxhSvYOok/heIOjhXLx3Z5oUTyy4=
github.com/juju/usso v1.0.1 h1:zyQhSUJnhFZdPqVAmPeqXYlnYXv+i0Cp1Ii+aziMXGs=
github.com/juju/usso v1.0.1/go.mod h1:3cvBcGVmWXyHhrBHBQtpNBzca/JRg4S5XH88Hj/NsYA=
github.com/juju/utils/v3 v3.1.1 h1:shEMr/4Wkw0YCOPz5IFOYkLv1ec50pzRi59TRl0qQ/0=
github.com/juju/utils/v3 v3.1.1/go.mod h1:nAj3sHtdYfAkvnkqttTy3Xzm2HzkD9Hfgnc+upOW2Z8=
github.com/juju/utils/v4 v4.0.2 h1:lh1cPruYCPLWBLuZpaAP18cc+3j9X/JsLExjwQ/+NxQ=
github.com/juju/utils/v4 v4.0.2/go.mod h1:j5wVHbRzw2LF85mb3H46cPPXBkyw5k4laDL6cOW55LY=
github.com/juju/version/v2 v2.0.1 h1:4psP9XDv8qIbSdv3llCmcmBXe+wvm57BvYp2sG/i6zc=
github.com/juju/version/v2 v2.0.1/go.mod h1:OK+DKO9ve3fFCVUZGT8ZbUWU2TnZh914S6gMhiM2hXg=
github.com/juju/webbrowser v1.0.0 h1:JLdmbFtCGY6Qf2jmS6bVaenJFGIFkdF1/BjUm76af78=
Expand Down
134 changes: 116 additions & 18 deletions internal/juju/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/juju/juju/core/network"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/rpc/params"
jujustorage "github.com/juju/juju/storage"
jujuversion "github.com/juju/juju/version"
"github.com/juju/names/v5"
"github.com/juju/retry"
Expand All @@ -59,6 +60,17 @@ func (ae *applicationNotFoundError) Error() string {
return fmt.Sprintf("application %s not found", ae.appName)
}

var StorageNotFoundError = &storageNotFoundError{}

// StorageNotFoundError
type storageNotFoundError struct {
storageName string
}

func (se *storageNotFoundError) Error() string {
return fmt.Sprintf("storage %s not found", se.storageName)
}

type applicationsClient struct {
SharedClient
controllerVersion version.Number
Expand Down Expand Up @@ -125,21 +137,22 @@ func ConfigEntryToString(input interface{}) string {
}

type CreateApplicationInput struct {
ApplicationName string
ModelName string
CharmName string
CharmChannel string
CharmBase string
CharmSeries string
CharmRevision int
Units int
Trust bool
Expose map[string]interface{}
Config map[string]string
Placement string
Constraints constraints.Value
EndpointBindings map[string]string
Resources map[string]int
ApplicationName string
ModelName string
CharmName string
CharmChannel string
CharmBase string
CharmSeries string
CharmRevision int
Units int
Trust bool
Expose map[string]interface{}
Config map[string]string
Placement string
Constraints constraints.Value
EndpointBindings map[string]string
Resources map[string]int
StorageContraints map[string]jujustorage.Constraints
}

// validateAndTransform returns transformedCreateApplicationInput which
Expand All @@ -156,6 +169,7 @@ func (input CreateApplicationInput) validateAndTransform(conn api.Connection) (p
parsed.trust = input.Trust
parsed.units = input.Units
parsed.resources = input.Resources
parsed.storage = input.StorageContraints

appName := input.ApplicationName
if appName == "" {
Expand Down Expand Up @@ -241,6 +255,7 @@ type transformedCreateApplicationInput struct {
trust bool
endpointBindings map[string]string
resources map[string]int
storage map[string]jujustorage.Constraints
}

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

Expand Down Expand Up @@ -364,6 +380,7 @@ func (c applicationsClient) deployFromRepository(applicationAPIClient *apiapplic
Revision: &transformedInput.charmRevision,
Trust: transformedInput.trust,
Resources: resources,
Storage: transformedInput.storage,
})
return errors.Join(errs...)
}
Expand Down Expand Up @@ -734,7 +751,7 @@ func (c applicationsClient) ReadApplicationWithRetryOnNotFound(ctx context.Conte
Func: func() error {
var err error
output, err = c.ReadApplication(input)
if errors.As(err, &ApplicationNotFoundError) {
if errors.As(err, &ApplicationNotFoundError) || errors.As(err, &StorageNotFoundError) {
return err
} else if err != nil {
// Log the error to the terraform Diagnostics to be
Expand All @@ -759,6 +776,18 @@ func (c applicationsClient) ReadApplicationWithRetryOnNotFound(ctx context.Conte
if len(machines) != output.Units {
return fmt.Errorf("ReadApplicationWithRetryOnNotFound: need %d machines, have %d", output.Units, len(machines))
}

// NOTE: Application can always have storages. However, they
// will not be listed right after the application is created. So
// we need to wait for the storages to be ready. And we need to
// check if all storage contraints have pool equal "" and size equal 0
// to drop the error.
for _, storage := range output.Storage {
if storage.Pool == "" || storage.Size == 0 {
return fmt.Errorf("ReadApplicationWithRetryOnNotFound: no storages found in output")
}
}

// NOTE: An IAAS subordinate should also have machines. However, they
// will not be listed until after the relation has been created.
// Those happen with the integration resource which will not be
Expand All @@ -785,6 +814,50 @@ func (c applicationsClient) ReadApplicationWithRetryOnNotFound(ctx context.Conte
return output, retryErr
}

func transformToStorageConstraints(
storageDetailsSlice []params.StorageDetails,
filesystemDetailsSlice []params.FilesystemDetails,
volumeDetailsSlice []params.VolumeDetails,
) map[string]jujustorage.Constraints {
storage := make(map[string]jujustorage.Constraints)
for _, storageDetails := range storageDetailsSlice {
// switch base on storage kind
storageCounters := make(map[string]uint64)
switch storageDetails.Kind.String() {
case "filesystem":
for _, fd := range filesystemDetailsSlice {
if fd.Storage.StorageTag == storageDetails.StorageTag {
// Cut 'storage-' prefix from the storage tag and `-NUMBER` suffix
storageLabel := getStorageLabel(storageDetails)
storageCounters[storageLabel]++
storage[storageLabel] = jujustorage.Constraints{
Pool: fd.Info.Pool,
Size: fd.Info.Size,
Count: storageCounters[storageLabel],
}
}
}
case "block":
for _, vd := range volumeDetailsSlice {
if vd.Storage.StorageTag == storageDetails.StorageTag {
storageLabel := getStorageLabel(storageDetails)
storageCounters[storageLabel]++
storage[storageLabel] = jujustorage.Constraints{
Pool: vd.Info.Pool,
Size: vd.Info.Size,
Count: storageCounters[storageLabel],
}
}
}
}
}
return storage
}

func getStorageLabel(storageDetails params.StorageDetails) string {
return strings.TrimSuffix(strings.TrimPrefix(storageDetails.StorageTag, "storage-"), "-0")
}

func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadApplicationResponse, error) {
conn, err := c.GetConnection(&input.ModelName)
if err != nil {
Expand Down Expand Up @@ -816,9 +889,17 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
appInfo := apps[0].Result

// TODO: Investigate why we're getting the full status here when
// application status is needed.
status, err := clientAPIClient.Status(nil)
// application status is needed. include storage.
status, err := clientAPIClient.Status(&apiclient.StatusArgs{
Patterns: []string{},
IncludeStorage: true,
})
if err != nil {
if strings.Contains(err.Error(), "filesystem for storage instance") {
// Retry if we get this error. It means the storage is not ready yet.
return nil, &storageNotFoundError{input.AppName}
}
c.Errorf(err, "failed to get status")
return nil, err
}
var appStatus params.ApplicationStatus
Expand All @@ -827,6 +908,22 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
return nil, fmt.Errorf("no status returned for application: %s", input.AppName)
}

//// Print Full status
//c.Tracef("Full status", map[string]interface{}{"status": status})
//
//// Get storage info from the full status.
//for _, sst := range status.StorageContraints {
// c.Tracef("StorageContraints status", map[string]interface{}{"storage": sst.StorageTag, "kind": sst.Kind})
//}
//for _, fst := range status.Filesystems {
// c.Tracef("Filesystem status", map[string]interface{}{"storage": fst.StorageContraints.StorageTag, "pool": fst.Info.Pool, "size": fst.Info.Size})
//}
storages := transformToStorageConstraints(status.Storage, status.Filesystems, status.Volumes)
// Print storage to console
for k, v := range storages {
c.Tracef("StorageContraints constraints", map[string]interface{}{"storage": k, "constraints": v})
}

allocatedMachines := set.NewStrings()
for _, v := range appStatus.Units {
if v.Machine != "" {
Expand Down Expand Up @@ -993,6 +1090,7 @@ func (c applicationsClient) ReadApplication(input *ReadApplicationInput) (*ReadA
Principal: appInfo.Principal,
Placement: placement,
EndpointBindings: endpointBindings,
Storage: storages,
Resources: resourceRevisions,
}

Expand Down
Loading

0 comments on commit 4c1b081

Please sign in to comment.