Skip to content

Commit e4a1590

Browse files
chore: Bring pinned_fcv feature into advanced_cluster TPF implementation (#2970)
* wip * small fix * adjusting model after update * add existing workaround * remove todos after verifying * set fcv changes into resulting model during create * remove create TODO after verifying and make testing adjustments * adjust checks to work with V2 schema * adjust checks to consider schema v2 attributes * small revert * move related functions in diags file * avoid creation of empty slice when not needed * refactor CheckRSAndDSSchemaV2 to reuse CheckRSAndDS for implementation * one liner for request definition * remove unnecessary isAcc from pinned fcv function
1 parent 5fd46c0 commit e4a1590

File tree

11 files changed

+254
-98
lines changed

11 files changed

+254
-98
lines changed

internal/common/conversion/error_framework.go renamed to internal/common/conversion/diags.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,25 @@ import (
44
"encoding/json"
55

66
"github.com/hashicorp/terraform-plugin-framework/diag"
7-
8-
legacyDiag "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7+
sdkv2diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
98
)
109

10+
func FromTPFDiagsToSDKV2Diags(diagsTpf []diag.Diagnostic) sdkv2diag.Diagnostics {
11+
var results []sdkv2diag.Diagnostic
12+
for _, tpfDiag := range diagsTpf {
13+
sdkV2Sev := sdkv2diag.Warning
14+
if tpfDiag.Severity() == diag.SeverityError {
15+
sdkV2Sev = sdkv2diag.Error
16+
}
17+
results = append(results, sdkv2diag.Diagnostic{
18+
Severity: sdkV2Sev,
19+
Summary: tpfDiag.Summary(),
20+
Detail: tpfDiag.Detail(),
21+
})
22+
}
23+
return results
24+
}
25+
1126
type ErrBody interface {
1227
Body() []byte
1328
}
@@ -34,13 +49,3 @@ func AddJSONBodyErrorToDiagnostics(msgPrefix string, err error, diags *diag.Diag
3449
errorJSON := string(errorBytes)
3550
diags.AddError(msgPrefix, errorJSON)
3651
}
37-
38-
func AddLegacyDiags(diags *diag.Diagnostics, legacyDiags legacyDiag.Diagnostics) {
39-
for _, diag := range legacyDiags {
40-
if diag.Severity == legacyDiag.Error {
41-
diags.AddError(diag.Summary, diag.Detail)
42-
} else {
43-
diags.AddWarning(diag.Summary, diag.Detail)
44-
}
45-
}
46-
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package conversion_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
sdkv2diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestFromTPFDiagsToSDKV2Diags(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
inputDiags []diag.Diagnostic
16+
expectedOutput sdkv2diag.Diagnostics
17+
}{
18+
{
19+
name: "Nil slice",
20+
inputDiags: nil,
21+
expectedOutput: nil,
22+
},
23+
{
24+
name: "Empty slice",
25+
inputDiags: []diag.Diagnostic{},
26+
expectedOutput: nil,
27+
},
28+
{
29+
name: "Single error diagnostic",
30+
inputDiags: []diag.Diagnostic{
31+
diag.NewErrorDiagnostic("Error summary", "Error detail"),
32+
},
33+
expectedOutput: []sdkv2diag.Diagnostic{
34+
{
35+
Severity: sdkv2diag.Error,
36+
Summary: "Error summary",
37+
Detail: "Error detail",
38+
},
39+
},
40+
},
41+
{
42+
name: "Mixed error and warning diagnostics",
43+
inputDiags: []diag.Diagnostic{
44+
diag.NewErrorDiagnostic("Error summary", "Error detail"),
45+
diag.NewWarningDiagnostic("Warning summary", "Warning detail"),
46+
},
47+
expectedOutput: []sdkv2diag.Diagnostic{
48+
{
49+
Severity: sdkv2diag.Error,
50+
Summary: "Error summary",
51+
Detail: "Error detail",
52+
},
53+
{
54+
Severity: sdkv2diag.Warning,
55+
Summary: "Warning summary",
56+
Detail: "Warning detail",
57+
},
58+
},
59+
},
60+
}
61+
62+
for _, tc := range tests {
63+
t.Run(tc.name, func(t *testing.T) {
64+
result := conversion.FromTPFDiagsToSDKV2Diags(tc.inputDiags)
65+
assert.Equal(t, tc.expectedOutput, result)
66+
})
67+
}
68+
}

internal/service/advancedcluster/resource_advanced_cluster.go

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
2727
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate"
2828
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
29+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf"
2930
)
3031

3132
const (
@@ -563,8 +564,10 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.
563564
}
564565

565566
if pinnedFCVBlock, _ := d.Get("pinned_fcv").([]any); len(pinnedFCVBlock) > 0 {
566-
if diags := PinFCV(ctx, connV2, projectID, clusterName, pinnedFCVBlock[0]); diags.HasError() {
567-
return diags
567+
nestedObj := pinnedFCVBlock[0].(map[string]any)
568+
expDateStr := cast.ToString(nestedObj["expiration_date"])
569+
if err := advancedclustertpf.PinFCV(ctx, connV2.ClustersApi, projectID, clusterName, expDateStr); err != nil {
570+
return diag.FromErr(fmt.Errorf(errorUpdate, clusterName, err))
568571
}
569572
waitForChanges = true
570573
}
@@ -789,30 +792,9 @@ func setRootFields(d *schema.ResourceData, cluster *admin.ClusterDescription2024
789792

790793
func WarningIfFCVExpiredOrUnpinnedExternally(d *schema.ResourceData, cluster *admin.ClusterDescription20240805) diag.Diagnostics {
791794
pinnedFCVBlock, _ := d.Get("pinned_fcv").([]any)
792-
presentInState := len(pinnedFCVBlock) > 0
793-
pinIsActive := cluster.FeatureCompatibilityVersionExpirationDate != nil
794-
if presentInState && !pinIsActive { // pin is not active but present in state (and potentially in config file)
795-
return diag.Diagnostics{
796-
diag.Diagnostic{
797-
Severity: diag.Warning,
798-
Summary: "FCV pin is no longer active",
799-
Detail: "Please remove `pinned_fcv` from the configuration and apply changes to avoid re-pinning the FCV. Warning can be ignored if `pinned_fcv` block has been removed from the configuration.",
800-
},
801-
}
802-
}
803-
if presentInState && pinIsActive {
804-
expirationDate := cluster.GetFeatureCompatibilityVersionExpirationDate()
805-
if time.Now().After(expirationDate) { // pin is active, present in state, but its expiration date has passed
806-
return diag.Diagnostics{
807-
diag.Diagnostic{
808-
Severity: diag.Warning,
809-
Summary: "FCV pin expiration date has expired",
810-
Detail: "During the next maintenance window FCV will be unpinned. FCV expiration date can be extended, or `pinned_fcv` block can be removed to trigger the unpin immediately.",
811-
},
812-
}
813-
}
814-
}
815-
return nil
795+
fcvPresentInState := len(pinnedFCVBlock) > 0
796+
diagsTpf := advancedclustertpf.GenerateFCVPinningWarningForRead(fcvPresentInState, cluster.FeatureCompatibilityVersionExpirationDate)
797+
return conversion.FromTPFDiagsToSDKV2Diags(diagsTpf)
816798
}
817799

818800
// isUsingOldShardingConfiguration is identified if at least one replication spec defines num_shards > 1. This legacy form is from 2023-02-01 API and can only represent symmetric sharded clusters.
@@ -986,8 +968,10 @@ func HandlePinnedFCVUpdate(ctx context.Context, connV2 *admin.APIClient, project
986968
isFCVPresentInConfig := len(pinnedFCVBlock) > 0
987969
if isFCVPresentInConfig {
988970
// pinned_fcv has been defined or updated expiration date
989-
if diags := PinFCV(ctx, connV2, projectID, clusterName, pinnedFCVBlock[0]); diags.HasError() {
990-
return diags
971+
nestedObj := pinnedFCVBlock[0].(map[string]any)
972+
expDateStr := cast.ToString(nestedObj["expiration_date"])
973+
if err := advancedclustertpf.PinFCV(ctx, connV2.ClustersApi, projectID, clusterName, expDateStr); err != nil {
974+
return diag.FromErr(fmt.Errorf(errorUpdate, clusterName, err))
991975
}
992976
} else {
993977
// pinned_fcv has been removed from the config so unpin method is called
@@ -1003,22 +987,6 @@ func HandlePinnedFCVUpdate(ctx context.Context, connV2 *admin.APIClient, project
1003987
return nil
1004988
}
1005989

1006-
func PinFCV(ctx context.Context, connV2 *admin.APIClient, projectID, clusterName string, fcvBlock any) diag.Diagnostics {
1007-
req := admin.PinFCV{}
1008-
if nestedObj, ok := fcvBlock.(map[string]any); ok {
1009-
expDateStrPtr := conversion.StringPtr(cast.ToString(nestedObj["expiration_date"]))
1010-
expirationTime, ok := conversion.StringPtrToTimePtr(expDateStrPtr)
1011-
if !ok {
1012-
return diag.FromErr(fmt.Errorf("expiration_date format is incorrect: %s", *expDateStrPtr))
1013-
}
1014-
req.ExpirationDate = expirationTime
1015-
}
1016-
if _, _, err := connV2.ClustersApi.PinFeatureCompatibilityVersion(ctx, projectID, clusterName, &req).Execute(); err != nil {
1017-
return diag.FromErr(fmt.Errorf(errorUpdate, clusterName, err))
1018-
}
1019-
return nil
1020-
}
1021-
1022990
func updateRequest(ctx context.Context, d *schema.ResourceData, projectID, clusterName string, connV2 *admin.APIClient) (*admin.ClusterDescription20240805, diag.Diagnostics) {
1023991
cluster := new(admin.ClusterDescription20240805)
1024992

internal/service/advancedcluster/resource_advanced_cluster_test.go

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,6 @@ func TestAccClusterAdvancedCluster_biConnectorConfig(t *testing.T) {
10131013
}
10141014

10151015
func TestAccClusterAdvancedCluster_pinnedFCVWithVersionUpgradeAndDowngrade(t *testing.T) {
1016-
acc.SkipIfAdvancedClusterV2Schema(t) // TODO: pinned_fcv not implemented in TPF yet
10171016
var (
10181017
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
10191018
projectName = acc.RandomProjectName() // Using single project to assert plural data source
@@ -1028,39 +1027,38 @@ func TestAccClusterAdvancedCluster_pinnedFCVWithVersionUpgradeAndDowngrade(t *te
10281027
eightDaysFromNow := sevenDaysFromNow.AddDate(0, 0, 1)
10291028
updatedExpirationDate := conversion.TimeToString(eightDaysFromNow)
10301029
invalidDateFormat := "invalid"
1031-
10321030
resource.ParallelTest(t, resource.TestCase{
10331031
PreCheck: func() { acc.PreCheckBasic(t) },
10341032
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
10351033
CheckDestroy: acc.CheckDestroyCluster,
10361034
Steps: []resource.TestStep{
10371035
{
1038-
Config: configFCVPinning(orgID, projectName, clusterName, nil, "7.0"),
1039-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 7, nil, nil),
1036+
Config: configFCVPinning(t, orgID, projectName, clusterName, nil, "7.0"),
1037+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 7, nil, nil),
10401038
},
10411039
{ // pins fcv
1042-
Config: configFCVPinning(orgID, projectName, clusterName, &firstExpirationDate, "7.0"),
1043-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(firstExpirationDate), admin.PtrInt(7)),
1040+
Config: configFCVPinning(t, orgID, projectName, clusterName, &firstExpirationDate, "7.0"),
1041+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(firstExpirationDate), admin.PtrInt(7)),
10441042
},
10451043
{ // using incorrect format
1046-
Config: configFCVPinning(orgID, projectName, clusterName, &invalidDateFormat, "7.0"),
1044+
Config: configFCVPinning(t, orgID, projectName, clusterName, &invalidDateFormat, "7.0"),
10471045
ExpectError: regexp.MustCompile("expiration_date format is incorrect: " + invalidDateFormat),
10481046
},
10491047
{ // updates expiration date of fcv
1050-
Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "7.0"),
1051-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
1048+
Config: configFCVPinning(t, orgID, projectName, clusterName, &updatedExpirationDate, "7.0"),
1049+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
10521050
},
10531051
{ // upgrade mongodb version with fcv pinned
1054-
Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "8.0"),
1055-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 8, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
1052+
Config: configFCVPinning(t, orgID, projectName, clusterName, &updatedExpirationDate, "8.0"),
1053+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 8, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
10561054
},
10571055
{ // downgrade mongodb version with fcv pinned
1058-
Config: configFCVPinning(orgID, projectName, clusterName, &updatedExpirationDate, "7.0"),
1059-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
1056+
Config: configFCVPinning(t, orgID, projectName, clusterName, &updatedExpirationDate, "7.0"),
1057+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 7, admin.PtrString(updatedExpirationDate), admin.PtrInt(7)),
10601058
},
10611059
{ // unpins fcv
1062-
Config: configFCVPinning(orgID, projectName, clusterName, nil, "7.0"),
1063-
Check: acc.CheckFCVPinningConfig(resourceName, dataSourceName, dataSourcePluralName, 7, nil, nil),
1060+
Config: configFCVPinning(t, orgID, projectName, clusterName, nil, "7.0"),
1061+
Check: acc.CheckFCVPinningConfig(true, resourceName, dataSourceName, dataSourcePluralName, 7, nil, nil),
10641062
},
10651063
},
10661064
})
@@ -1334,13 +1332,9 @@ func configSharded(t *testing.T, projectID, clusterName string, withUpdate bool)
13341332
}
13351333

13361334
func checkAggr(isAcc bool, attrsSet []string, attrsMap map[string]string, extra ...resource.TestCheckFunc) resource.TestCheckFunc {
1337-
checks := []resource.TestCheckFunc{acc.CheckExistsCluster(resourceName)}
1338-
checks = acc.AddAttrChecksSchemaV2(isAcc, resourceName, checks, attrsMap)
1339-
checks = acc.AddAttrSetChecksSchemaV2(isAcc, resourceName, checks, attrsSet...)
1340-
checks = acc.AddAttrChecksSchemaV2(isAcc, dataSourceName, checks, attrsMap)
1341-
checks = acc.AddAttrSetChecksSchemaV2(isAcc, dataSourceName, checks, attrsSet...)
1342-
checks = append(checks, extra...)
1343-
return resource.ComposeAggregateTestCheckFunc(checks...)
1335+
extraChecks := extra
1336+
extraChecks = append(extraChecks, acc.CheckExistsCluster(resourceName))
1337+
return acc.CheckRSAndDSSchemaV2(isAcc, resourceName, admin.PtrString(dataSourceName), nil, attrsSet, attrsMap, extraChecks...)
13441338
}
13451339

13461340
func configTenant(t *testing.T, isAcc bool, projectID, name, zoneName string) string {
@@ -2745,7 +2739,8 @@ func checkTenantBiConnectorConfig(isAcc bool, projectID, name string, enabled bo
27452739
return checkAggr(isAcc, nil, attrsMap)
27462740
}
27472741

2748-
func configFCVPinning(orgID, projectName, clusterName string, pinningExpirationDate *string, mongoDBMajorVersion string) string {
2742+
func configFCVPinning(t *testing.T, orgID, projectName, clusterName string, pinningExpirationDate *string, mongoDBMajorVersion string) string {
2743+
t.Helper()
27492744
var pinnedFCVAttr string
27502745
if pinningExpirationDate != nil {
27512746
pinnedFCVAttr = fmt.Sprintf(`
@@ -2755,7 +2750,7 @@ func configFCVPinning(orgID, projectName, clusterName string, pinningExpirationD
27552750
`, *pinningExpirationDate)
27562751
}
27572752

2758-
return fmt.Sprintf(`
2753+
return acc.ConvertAdvancedClusterToSchemaV2(t, true, fmt.Sprintf(`
27592754
resource "mongodbatlas_project" "test" {
27602755
org_id = %[1]q
27612756
name = %[2]q
@@ -2784,7 +2779,7 @@ func configFCVPinning(orgID, projectName, clusterName string, pinningExpirationD
27842779
}
27852780
}
27862781
2787-
`, orgID, projectName, clusterName, mongoDBMajorVersion, pinnedFCVAttr) + dataSourcesTFNewSchema
2782+
`, orgID, projectName, clusterName, mongoDBMajorVersion, pinnedFCVAttr)) + dataSourcesTFNewSchema
27882783
}
27892784

27902785
func importIgnoredFields() []string {

internal/service/advancedclustertpf/common.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package advancedclustertpf
22

33
import (
4+
"context"
45
"fmt"
56
"strings"
7+
"time"
68

9+
"github.com/hashicorp/terraform-plugin-framework/diag"
710
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant"
11+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
812
"github.com/spf13/cast"
913
"go.mongodb.org/atlas-sdk/v20241113004/admin"
1014
)
@@ -44,3 +48,36 @@ func GetAdvancedClusterContainerID(containers []admin.CloudProviderContainer, cl
4448
}
4549
return ""
4650
}
51+
52+
func PinFCV(ctx context.Context, api admin.ClustersApi, projectID, clusterName, expirationDateStr string) error {
53+
expirationTime, ok := conversion.StringToTime(expirationDateStr)
54+
if !ok {
55+
return fmt.Errorf("expiration_date format is incorrect: %s", expirationDateStr)
56+
}
57+
req := admin.PinFCV{
58+
ExpirationDate: &expirationTime,
59+
}
60+
if _, _, err := api.PinFeatureCompatibilityVersion(ctx, projectID, clusterName, &req).Execute(); err != nil {
61+
return err
62+
}
63+
return nil
64+
}
65+
66+
func GenerateFCVPinningWarningForRead(fcvPresentInState bool, apiRespFCVExpirationDate *time.Time) []diag.Diagnostic {
67+
pinIsActive := apiRespFCVExpirationDate != nil
68+
if fcvPresentInState && !pinIsActive { // pin is not active but present in state (and potentially in config file)
69+
warning := diag.NewWarningDiagnostic(
70+
"FCV pin is no longer active",
71+
"Please remove `pinned_fcv` from the configuration and apply changes to avoid re-pinning the FCV. Warning can be ignored if `pinned_fcv` block has been removed from the configuration.")
72+
return []diag.Diagnostic{warning}
73+
}
74+
if fcvPresentInState && pinIsActive {
75+
if time.Now().After(*apiRespFCVExpirationDate) { // pin is active, present in state, but its expiration date has passed
76+
warning := diag.NewWarningDiagnostic(
77+
"FCV pin expiration date has expired",
78+
"During the next maintenance window FCV will be unpinned. FCV expiration date can be extended, or `pinned_fcv` block can be removed to trigger the unpin immediately.")
79+
return []diag.Diagnostic{warning}
80+
}
81+
}
82+
return nil
83+
}

internal/service/advancedclustertpf/model_ClusterDescription20240805.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewTFModel(ctx context.Context, input *admin.ClusterDescription20240805, ti
3434
labels := NewLabelsObjType(ctx, input.Labels, diags)
3535
replicationSpecs := NewReplicationSpecsObjType(ctx, input.ReplicationSpecs, diags, &apiInfo)
3636
tags := NewTagsObjType(ctx, input.Tags, diags)
37+
pinnedFCV := NewPinnedFCVObjType(ctx, input, diags)
3738
if diags.HasError() {
3839
return nil
3940
}
@@ -65,7 +66,7 @@ func NewTFModel(ctx context.Context, input *admin.ClusterDescription20240805, ti
6566
Tags: tags,
6667
TerminationProtectionEnabled: types.BoolValue(conversion.SafeValue(input.TerminationProtectionEnabled)),
6768
VersionReleaseSystem: types.StringValue(conversion.SafeValue(input.VersionReleaseSystem)),
68-
PinnedFCV: types.ObjectNull(PinnedFCVObjType.AttrTypes), // TODO static object
69+
PinnedFCV: pinnedFCV,
6970
Timeouts: timeout,
7071
}
7172
}
@@ -134,6 +135,19 @@ func NewReplicationSpecsObjType(ctx context.Context, input *[]admin.ReplicationS
134135
return listType
135136
}
136137

138+
func NewPinnedFCVObjType(ctx context.Context, cluster *admin.ClusterDescription20240805, diags *diag.Diagnostics) types.Object {
139+
if cluster.FeatureCompatibilityVersionExpirationDate == nil {
140+
return types.ObjectNull(PinnedFCVObjType.AttrTypes)
141+
}
142+
tfModel := TFPinnedFCVModel{
143+
Version: types.StringValue(cluster.GetFeatureCompatibilityVersion()),
144+
ExpirationDate: types.StringValue(conversion.TimeToString(cluster.GetFeatureCompatibilityVersionExpirationDate())),
145+
}
146+
objType, diagsLocal := types.ObjectValueFrom(ctx, PinnedFCVObjType.AttrTypes, tfModel)
147+
diags.Append(diagsLocal...)
148+
return objType
149+
}
150+
137151
func convertReplicationSpecs(ctx context.Context, input *[]admin.ReplicationSpec20240805, diags *diag.Diagnostics, apiInfo *ExtraAPIInfo) *[]TFReplicationSpecsModel {
138152
tfModels := make([]TFReplicationSpecsModel, len(*input))
139153
for i, item := range *input {

0 commit comments

Comments
 (0)