Skip to content

Commit

Permalink
reafactor: many small fixes accross all storage functions.
Browse files Browse the repository at this point in the history
refactor: start using juju/utils/v3 insead v4

chore: fix commentary for full status reasoning

test: add unit test for StorageNotFound error

chore: fix StorageContraints typo to StorageConstraints.

chore: fix resource_application.go imports.

chore: add comments to the storageSetRequiresReplace func.

chore: Move from ValueSting to Sting in Sprintf

chore: misseing error check

chore: add returning errors to resp.Diagnostics

refactor: move update storage to a separate method

refactor: change channel on revision, not to break test in the future.

refactor: move storageSetRequiresReplace to separate file

fix: unit test for Storage not found (retry logic)

fix: golangci-lint small fixes.

chore: fix copyright issue.
  • Loading branch information
anvial committed Jun 24, 2024
1 parent 36007c5 commit af585a1
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 201 deletions.
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) StorageContraints constraints for disks to attach to the machine(s).
- `disks` (String) Storage 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
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ 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 @@ -125,7 +124,6 @@ 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: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,6 @@ github.com/juju/juju v0.0.0-20240524040137-95c267441801 h1:5aNKt2VkNKsS2JI2QcCjN
github.com/juju/juju v0.0.0-20240524040137-95c267441801/go.mod h1:dxhhMNnbWSXUB7EdQEsaxLhgwKQXo2Ctl4z4AZwWL88=
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,16 +400,14 @@ 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.2.0 h1:Q0wxjaxx4XPVEN+SgzxKr3d82pjmSBcuM3WndAU391c=
github.com/juju/testing v1.2.0/go.mod h1:lqZVzNwBKAbylGZidK77ts6kIdoOkmD52+4m0ysetPo=
github.com/juju/testing v1.1.0 h1:+WWez0vCu6dtnpLIzfuuo3bN3x62LBIyMDCfvMYP+Qg=
github.com/juju/testing v1.1.0/go.mod h1:1XQGptw6JWFvRWb3ewilUdTBG0oGcoI2kdX9Z1VEzhU=
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
54 changes: 23 additions & 31 deletions internal/juju/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +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
StorageContraints map[string]jujustorage.Constraints
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
StorageConstraints map[string]jujustorage.Constraints
}

// validateAndTransform returns transformedCreateApplicationInput which
Expand All @@ -169,7 +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
parsed.storage = input.StorageConstraints

appName := input.ApplicationName
if appName == "" {
Expand Down Expand Up @@ -781,7 +781,7 @@ func (c applicationsClient) ReadApplicationWithRetryOnNotFound(ctx context.Conte
return fmt.Errorf("ReadApplicationWithRetryOnNotFound: need %d machines, have %d", output.Units, len(machines))
}

// NOTE: Application can always have storages. However, they
// NOTE: Application can always have storage. 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 constraints have pool equal "" and size equal 0
Expand Down Expand Up @@ -892,8 +892,10 @@ 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. include storage.
// We are currently retrieving the full status, which might be more information than necessary.
// The main focus is on the application status, particularly including the storage status.
// It might be more efficient to directly query for the application status and its associated storage status.
// TODO: Investigate if there's a way to optimize this by only fetching the required information.
status, err := clientAPIClient.Status(&apiclient.StatusArgs{
Patterns: []string{},
IncludeStorage: true,
Expand All @@ -912,20 +914,10 @@ 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})
c.Tracef("StorageConstraints constraints", map[string]interface{}{"storage": k, "constraints": v})
}

allocatedMachines := set.NewStrings()
Expand Down
66 changes: 66 additions & 0 deletions internal/juju/applications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,72 @@ func (s *ApplicationSuite) TestReadApplicationRetrySubordinate() {
s.Assert().Equal("[email protected]", resp.Base)
}

// TestReadApplicationRetryNotFoundStorageNotFoundError tests the case where the first response is a storage not found error.
// The second response is a real application.
func (s *ApplicationSuite) TestReadApplicationRetryNotFoundStorageNotFoundError() {
defer s.setupMocks(s.T()).Finish()
s.mockSharedClient.EXPECT().ModelType(gomock.Any()).Return(model.IAAS, nil).AnyTimes()

appName := "testapplication"
aExp := s.mockApplicationClient.EXPECT()

// First response is a storage not found error.
aExp.ApplicationsInfo(gomock.Any()).Return([]params.ApplicationInfoResult{{
Error: &params.Error{Message: `storage "testapplication" not found`, Code: "not found"},
}}, nil)

// Retry - expect ApplicationsInfo and Status to be called.
// The second time return a real application.
amdConst := constraints.MustParse("arch=amd64")
infoResult := params.ApplicationInfoResult{
Result: &params.ApplicationResult{
Tag: names.NewApplicationTag(appName).String(),
Charm: "ch:amd64/jammy/testcharm-5",
Base: params.Base{Name: "ubuntu", Channel: "22.04"},
Channel: "stable",
Constraints: amdConst,
Principal: true,
},
Error: nil,
}

aExp.ApplicationsInfo(gomock.Any()).Return([]params.ApplicationInfoResult{infoResult}, nil)
getResult := &params.ApplicationGetResults{
Application: appName,
CharmConfig: nil,
ApplicationConfig: nil,
Charm: "ch:amd64/jammy/testcharm-5",
Base: params.Base{Name: "ubuntu", Channel: "22.04"},
Channel: "stable",
Constraints: amdConst,
EndpointBindings: nil,
}
aExp.Get("master", appName).Return(getResult, nil)
statusResult := &params.FullStatus{
Applications: map[string]params.ApplicationStatus{appName: {
Charm: "ch:amd64/jammy/testcharm-5",
Units: map[string]params.UnitStatus{"testapplication/0": {
Machine: "0",
}},
}},
}
s.mockClient.EXPECT().Status(gomock.Any()).Return(statusResult, nil)

client := s.getApplicationsClient()
resp, err := client.ReadApplicationWithRetryOnNotFound(context.Background(),
&ReadApplicationInput{
ModelName: s.testModelName,
AppName: appName,
})
s.Require().NoError(err, "error from ReadApplicationWithRetryOnNotFound")
s.Require().NotNil(resp, "ReadApplicationWithRetryOnNotFound response")

s.Assert().Equal("testcharm", resp.Name)
s.Assert().Equal("stable", resp.Channel)
s.Assert().Equal(5, resp.Revision)
s.Assert().Equal("[email protected]", resp.Base)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestApplicationSuite(t *testing.T) {
Expand Down
103 changes: 103 additions & 0 deletions internal/provider/modifer_storageset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
jujustorage "github.com/juju/juju/storage"
"github.com/juju/utils/v3"
)

// storageSetRequiresReplace is a plan modifier function that determines if the storage set requires a replace.
// It compares the storage set in the plan with the storage set in the state.
// Return false if new items were added and old items were not changed.
// Return true if old items were removed
func storageSetRequiresReplace(ctx context.Context, req planmodifier.SetRequest, resp *setplanmodifier.RequiresReplaceIfFuncResponse) {
planSet := make(map[string]jujustorage.Constraints)
if !req.PlanValue.IsNull() {
var planStorageSlice []nestedStorage
resp.Diagnostics.Append(req.PlanValue.ElementsAs(ctx, &planStorageSlice, false)...)
if resp.Diagnostics.HasError() {
return
}
if len(planStorageSlice) > 0 {
for _, storage := range planStorageSlice {
storageName := storage.Label.ValueString()
storageSize := storage.Size.ValueString()
storagePool := storage.Pool.ValueString()
storageCount := storage.Count.ValueInt64()

// Validate storage size
parsedStorageSize, err := utils.ParseSize(storageSize)
if err != nil {
resp.Diagnostics.AddError("Invalid Storage Size", fmt.Sprintf("Invalid storage size %q: %s", storageSize, err))
return
}

planSet[storageName] = jujustorage.Constraints{
Size: parsedStorageSize,
Pool: storagePool,
Count: uint64(storageCount),
}
}
}
}

stateSet := make(map[string]jujustorage.Constraints)
if !req.StateValue.IsNull() {
var stateStorageSlice []nestedStorage
resp.Diagnostics.Append(req.StateValue.ElementsAs(ctx, &stateStorageSlice, false)...)
if resp.Diagnostics.HasError() {
return
}
if len(stateStorageSlice) > 0 {
for _, storage := range stateStorageSlice {
storageName := storage.Label.ValueString()
storageSize := storage.Size.ValueString()
storagePool := storage.Pool.ValueString()
storageCount := storage.Count.ValueInt64()

// Validate storage size
parsedStorageSize, err := utils.ParseSize(storageSize)
if err != nil {
resp.Diagnostics.AddError("Invalid Storage Size", fmt.Sprintf("Invalid storage size %q: %s", storageSize, err))
return
}

stateSet[storageName] = jujustorage.Constraints{
Size: parsedStorageSize,
Pool: storagePool,
Count: uint64(storageCount),
}
}
}
}

// Return false if new items were added and old items were not changed
for key, value := range planSet {
stateValue, ok := stateSet[key]
if !ok {
resp.RequiresReplace = false
return
}
if (value.Size != stateValue.Size) || (value.Pool != stateValue.Pool) || (value.Count != stateValue.Count) {
resp.RequiresReplace = true
return
}
}

// Return true if old items were removed
for key := range stateSet {
if _, ok := planSet[key]; !ok {
resp.RequiresReplace = true
return
}
}

resp.RequiresReplace = false
}
Loading

0 comments on commit af585a1

Please sign in to comment.