From 13868b6a8429bb2b587ef47512cb3158c19e5423 Mon Sep 17 00:00:00 2001 From: Dimitar Dimitrov Date: Tue, 12 Sep 2023 17:13:17 +0300 Subject: [PATCH] PRODENG-2237 Added pruning policy resource Signed-off-by: Dimitar Dimitrov --- docs/resources/pruning_policy.md | 39 +++ internal/client/pruning_policy.go | 118 ++++++++ internal/client/pruning_policy_test.go | 170 ++++++++++++ internal/client/utils.go | 54 +++- internal/provider/accounts_data_source.go | 47 ---- internal/provider/provider.go | 3 +- internal/provider/pruning_policy_resource.go | 253 ++++++++++++++++++ .../provider/pruning_policy_resource_test.go | 82 ++++++ 8 files changed, 716 insertions(+), 50 deletions(-) create mode 100644 docs/resources/pruning_policy.md create mode 100644 internal/client/pruning_policy.go create mode 100644 internal/client/pruning_policy_test.go create mode 100644 internal/provider/pruning_policy_resource.go create mode 100644 internal/provider/pruning_policy_resource_test.go diff --git a/docs/resources/pruning_policy.md b/docs/resources/pruning_policy.md new file mode 100644 index 0000000..c3e1a2e --- /dev/null +++ b/docs/resources/pruning_policy.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "msr_pruning_policy Resource - terraform-provider-msr" +subcategory: "" +description: |- + Pruning policy resource +--- + +# msr_pruning_policy (Resource) + +Pruning policy resource + + + + +## Schema + +### Required + +- `org_name` (String) The organization that contains the repo +- `repo_name` (String) The repository to apply the pruning policy on + +### Optional + +- `enabled` (Boolean) Is the pruning policy enabled +- `rule` (Block List) The rules of the pruning policy (see [below for nested schema](#nestedblock--rule)) + +### Read-Only + +- `id` (String) Identifier + + +### Nested Schema for `rule` + +Required: + +- `field` (String) The field for the rule +- `operator` (String) The operator for the particular field +- `values` (List of String) The regex values for the rule diff --git a/internal/client/pruning_policy.go b/internal/client/pruning_policy.go new file mode 100644 index 0000000..6e4ea1d --- /dev/null +++ b/internal/client/pruning_policy.go @@ -0,0 +1,118 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type PruningPolicyRuleAPI struct { + Field string `tfsdk:"field" json:"field"` + Operator string `tfsdk:"operator" json:"operator"` + Values []string `tfsdk:"values" json:"values"` +} + +type PruningPolicyRuleTFSDK struct { + Field types.String `tfsdk:"field" json:"field"` + Operator types.String `tfsdk:"operator" json:"operator"` + Values []types.String `tfsdk:"values" json:"values"` +} + +type CreatePruningPolicy struct { + Enabled bool `json:"enabled"` + Rules []PruningPolicyRuleAPI `json:"rules"` +} + +type ResponsePruningPolicy struct { + ID string `json:"id"` + Enabled bool `json:"enabled"` + Rules []PruningPolicyRuleAPI `json:"rules"` +} + +// CreatePruningPolicy creates a repo in MSR. +func (c *Client) CreatePruningPolicy(ctx context.Context, orgName string, repoName string, policy CreatePruningPolicy) (ResponsePruningPolicy, error) { + body, err := json.Marshal(policy) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("creating pruning policy %+v failed. %w: %s", policy, ErrMarshaling, err) + } + url := fmt.Sprintf("%s/%s/%s/pruningPolicies?initialEvaluation=true", c.createMsrUrl("repositories"), orgName, repoName) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("creating pruning policy %+v failed. %w: %s", policy, ErrRequestCreation, err) + } + req.Header.Set("Content-Type", "application/json") + resBody, err := c.doRequest(req) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("creating pruning policy %+v failed. %w", policy, err) + } + + resPolicy := ResponsePruningPolicy{} + if err := json.Unmarshal(resBody, &resPolicy); err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("creating pruning policy %+v failed. %w: %s", policy, ErrUnmarshaling, err) + } + + return resPolicy, nil +} + +// ReadPruningPolicy creates a repo in MSR. +func (c *Client) ReadPruningPolicy(ctx context.Context, orgName string, repoName string, policyId string) (ResponsePruningPolicy, error) { + url := fmt.Sprintf("%s/%s/%s/pruningPolicies/%s", c.createMsrUrl("repositories"), orgName, repoName, policyId) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("reading pruning policy for %s/%s failed. %w: %s", orgName, repoName, ErrRequestCreation, err) + } + resBody, err := c.doRequest(req) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("reading pruning policy for %s/%s failed. %w", orgName, repoName, err) + } + + resPolicy := ResponsePruningPolicy{} + if err := json.Unmarshal(resBody, &resPolicy); err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("reading pruning policy for %s/%s failed. %w: %s", orgName, repoName, ErrUnmarshaling, err) + } + + return resPolicy, nil +} + +// DeletePruningPolicy creates a repo in MSR. +func (c *Client) DeletePruningPolicy(ctx context.Context, orgName string, repoName string, policyId string) error { + url := fmt.Sprintf("%s/%s/%s/pruningPolicies/%s", c.createMsrUrl("repositories"), orgName, repoName, policyId) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) + if err != nil { + return fmt.Errorf("deleting pruning policy for %s/%s failed. %w: %s", orgName, repoName, ErrRequestCreation, err) + } + if _, err = c.doRequest(req); err != nil { + return fmt.Errorf("deleting pruning policy for %s/%s failed. %w", orgName, repoName, err) + } + + return err +} + +// UpdatePruningPolicy creates a repo in MSR. +func (c *Client) UpdatePruningPolicy(ctx context.Context, orgName string, repoName string, policy CreatePruningPolicy, policyId string) (ResponsePruningPolicy, error) { + body, err := json.Marshal(policy) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("creating pruning policy %+v failed. %w: %s", policy, ErrMarshaling, err) + } + url := fmt.Sprintf("%s/%s/%s/pruningPolicies/%s?initialEvaluation=true", c.createMsrUrl("repositories"), orgName, repoName, policyId) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewBuffer(body)) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("updating pruning policy for %s/%s failed. %w: %s", orgName, repoName, ErrRequestCreation, err) + } + req.Header.Set("Content-Type", "application/json") + resBody, err := c.doRequest(req) + if err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("updating pruning policy for %s/%s failed. %w", orgName, repoName, err) + } + + resPolicy := ResponsePruningPolicy{} + if err := json.Unmarshal(resBody, &resPolicy); err != nil { + return ResponsePruningPolicy{}, fmt.Errorf("updating pruning policy for %s/%s failed. %w: %s", orgName, repoName, ErrUnmarshaling, err) + } + + return resPolicy, nil +} diff --git a/internal/client/pruning_policy_test.go b/internal/client/pruning_policy_test.go new file mode 100644 index 0000000..ed3e656 --- /dev/null +++ b/internal/client/pruning_policy_test.go @@ -0,0 +1,170 @@ +package client_test + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/Mirantis/terraform-provider-msr/internal/client" +) + +type testPruningPolicyStruct struct { + server *httptest.Server + expectedResponse client.ResponsePruningPolicy + expectedErr error +} + +func TestCreateValidPruningPolicy(t *testing.T) { + testResPolicy := client.ResponsePruningPolicy{ + ID: "fake-test-id", + Enabled: true, + Rules: []client.PruningPolicyRuleAPI{ + { + Field: "tag", + Operator: "eq", + Values: []string{"test"}, + }, + }, + } + mAccount, err := json.Marshal(testResPolicy) + if err != nil { + t.Error(err) + } + tc := testPruningPolicyStruct{ + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write(mAccount); err != nil { + t.Error(err) + return + } + })), + expectedResponse: testResPolicy, + expectedErr: nil, + } + defer tc.server.Close() + testClient, err := client.NewDefaultClient(tc.server.URL, "fakeuser", "fakepass", true) + if err != nil { + t.Error("couldn't create test client") + } + ctx := context.Background() + resp, err := testClient.CreatePruningPolicy(ctx, "fake", "fake", client.CreatePruningPolicy{ + Enabled: true, + Rules: []client.PruningPolicyRuleAPI{ + { + Field: "tag", + Operator: "eq", + Values: []string{"test"}, + }, + }, + }) + if !reflect.DeepEqual(tc.expectedResponse, resp) { + t.Errorf("expected (%v), got (%v)", tc.expectedResponse, resp) + } + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected (%v), got (%v)", tc.expectedErr, err) + } +} + +func TestCreateInvalidPruningPolicy(t *testing.T) { + testResPolicy := client.ResponsePruningPolicy{} + mAccount, err := json.Marshal(testResPolicy) + if err != nil { + t.Fatal(err) + } + tc := testPruningPolicyStruct{ + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write(mAccount); err != nil { + t.Error(err) + return + } + })), + expectedResponse: testResPolicy, + expectedErr: client.ErrEmptyResError, + } + defer tc.server.Close() + testClient, err := client.NewDefaultClient(tc.server.URL, "fakeuser", "fakepass", true) + if err != nil { + t.Error("couldn't create test client") + } + ctx := context.Background() + + resp, err := testClient.CreatePruningPolicy(ctx, "fake", "fake", client.CreatePruningPolicy{ + Enabled: true, + Rules: []client.PruningPolicyRuleAPI{ + { + Field: "tag", + Operator: "eq", + Values: []string{"test"}, + }, + }, + }) + + if !reflect.DeepEqual(tc.expectedResponse, resp) { + t.Errorf("expected resp: (%+v),\n got (%+v)", tc.expectedResponse, resp) + } + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected error: (%v),\n got (%v)", tc.expectedErr, err) + } +} + +func TestCreatePruningPolicyErrUmarshaling(t *testing.T) { + tc := testPruningPolicyStruct{ + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write(nil); err != nil { + t.Error(err) + return + } + })), + expectedResponse: client.ResponsePruningPolicy{}, + expectedErr: client.ErrUnmarshaling, + } + defer tc.server.Close() + testClient, err := client.NewDefaultClient(tc.server.URL, "fakeuser", "fakepass", true) + if err != nil { + t.Error("couldn't create test client") + } + ctx := context.Background() + + resp, err := testClient.CreatePruningPolicy(ctx, "fake", "fake", client.CreatePruningPolicy{}) + + if !reflect.DeepEqual(tc.expectedResponse, resp) { + t.Errorf("expected resp: (%+v),\n got (%+v)", tc.expectedResponse, resp) + } + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected error: (%v),\n got (%v)", tc.expectedErr, err) + } +} + +func TestUpdatePruningPolicyFailed(t *testing.T) { + tc := testPruningPolicyStruct{ + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write(nil); err != nil { + t.Error(err) + return + } + })), + expectedResponse: client.ResponsePruningPolicy{}, + expectedErr: client.ErrUnmarshaling, + } + defer tc.server.Close() + testClient, err := client.NewDefaultClient(tc.server.URL, "fakeuser", "fakepass", true) + if err != nil { + t.Error("couldn't create test client") + } + ctx := context.Background() + resp, err := testClient.UpdatePruningPolicy(ctx, "fakeid", "fakeid", client.CreatePruningPolicy{Enabled: true}, "fakeid") + + if !reflect.DeepEqual(tc.expectedResponse, resp) { + t.Errorf("expected resp: (%+v),\n got (%+v)", tc.expectedResponse, resp) + } + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected error: (%v),\n got (%v)", tc.expectedErr, err) + } +} diff --git a/internal/client/utils.go b/internal/client/utils.go index fa5de4a..afdfbf0 100644 --- a/internal/client/utils.go +++ b/internal/client/utils.go @@ -1,17 +1,69 @@ package client import ( + "context" "math/rand" "time" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // GeneratePass creates a random password. func GeneratePass() string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$") - rand.Seed(time.Now().UnixNano()) + rand.New(rand.NewSource(time.Now().UnixNano())) b := make([]rune, 8) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } + +// PruningPolicyRulesToAPI converts slice of PruningPolicyRuleTFSDK rules to PruningPolicyRuleAPI. +func PruningPolicyRulesToAPI(ctx context.Context, rules []PruningPolicyRuleTFSDK) []PruningPolicyRuleAPI { + // Convert the TFSDK rules to API rules + var apiRules []PruningPolicyRuleAPI + + for _, r := range rules { + apiRules = append(apiRules, r.PruningPolicyRuleToAPI(ctx)) + } + return apiRules +} + +// PruningPolicyRulesToTFSDK converts slice of PruningPolicyRuleAPI rules to PruningPolicyRuleTFSDK. +func PruningPolicyRulesToTFSDK(ctx context.Context, rules []PruningPolicyRuleAPI) []PruningPolicyRuleTFSDK { + // Convert the API rules to TFSDK rules + var apiRules []PruningPolicyRuleTFSDK + + for _, r := range rules { + apiRules = append(apiRules, r.PruningPolicyRuleToTFSDK(ctx)) + } + return apiRules +} + +// PruningPolicyRuleToAPI converts a single PruningPolicyRuleTFSDK rule to PruningPolicyRuleAPI. +func (r *PruningPolicyRuleTFSDK) PruningPolicyRuleToAPI(ctx context.Context) PruningPolicyRuleAPI { + var values []string + for _, v := range r.Values { + values = append(values, v.String()) + } + return PruningPolicyRuleAPI{ + Field: r.Field.String(), + Operator: r.Operator.String(), + Values: values, + } +} + +// PruningPolicyRuleToTFSDK converts a single PruningPolicyRuleAPI rule to PruningPolicyRuleToTFSDK. +func (r *PruningPolicyRuleAPI) PruningPolicyRuleToTFSDK(ctx context.Context) PruningPolicyRuleTFSDK { + var values []types.String + for _, v := range r.Values { + values = append(values, basetypes.NewStringValue(v)) + } + return PruningPolicyRuleTFSDK{ + Field: basetypes.NewStringValue(r.Field), + Operator: basetypes.NewStringValue(r.Operator), + Values: values, + } +} diff --git a/internal/provider/accounts_data_source.go b/internal/provider/accounts_data_source.go index 60ae495..2e75305 100644 --- a/internal/provider/accounts_data_source.go +++ b/internal/provider/accounts_data_source.go @@ -1,50 +1,3 @@ -// func dataSourceAccountsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { -// c, ok := m.(client.Client) -// if !ok { -// return diag.Errorf("unable to cast meta interface to MSR Client") -// } - -// filter := client.AccountFilter("all") -// if _, ok := d.GetOk("filter"); ok { -// inputFilter := d.Get("filter").(string) -// filter = client.AccountFilter(inputFilter) - -// if filter.APIFormOfFilter() != inputFilter { -// d.SetId("") -// return diag.FromErr(fmt.Errorf("%w. Filter '%s'", client.ErrInvalidFilter, inputFilter)) -// } -// } -// rAccounts, err := c.ReadAccounts(ctx, filter) -// if err != nil { -// // If the accounts doesn't exist we should gracefully handle it -// d.SetId("") -// return diag.FromErr(err) -// } - -// accounts := make([]map[string]interface{}, 0, len(rAccounts)) - -// for _, u := range rAccounts { -// accounts = append(accounts, map[string]interface{}{ -// "id": u.ID, -// "name": u.Name, -// "full_name": u.FullName, -// "is_active": u.IsActive, -// "is_admin": u.IsAdmin, -// "is_org": u.IsOrg, -// "members_count": u.MembersCount, -// "teams_count": u.TeamsCount, -// }) -// } - -// if err := d.Set("accounts", accounts); err != nil { -// return diag.FromErr(err) -// } - -// d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) - -// return diag.Diagnostics{} -// } - package provider import ( diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f49d4b1..b7e307e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,8 +3,6 @@ package provider import ( "context" - // "net/http" - "github.com/Mirantis/terraform-provider-msr/internal/client" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -113,6 +111,7 @@ func (p *MSRProvider) Resources(ctx context.Context) []func() resource.Resource NewUserResource, NewTeamResource, NewRepoResource, + NewPruningPolicyResource, } } diff --git a/internal/provider/pruning_policy_resource.go b/internal/provider/pruning_policy_resource.go new file mode 100644 index 0000000..9c31f0b --- /dev/null +++ b/internal/provider/pruning_policy_resource.go @@ -0,0 +1,253 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/Mirantis/terraform-provider-msr/internal/client" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var _ resource.Resource = &PruningPolicyResource{} + +type PruningPolicyResourceModel struct { + Id types.String `tfsdk:"id"` + Enabled types.Bool `tfsdk:"enabled"` + OrgName types.String `tfsdk:"org_name"` + RepoName types.String `tfsdk:"repo_name"` + Rules []client.PruningPolicyRuleTFSDK `tfsdk:"rule"` +} + +type PruningPolicyResource struct { + client client.Client +} + +func NewPruningPolicyResource() resource.Resource { + return &PruningPolicyResource{} +} + +func (r *PruningPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pruning_policy" + +} + +func (r *PruningPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Pruning policy resource", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Is the pruning policy enabled", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "org_name": schema.StringAttribute{ + MarkdownDescription: "The organization that contains the repo", + Required: true, + }, + "repo_name": schema.StringAttribute{ + MarkdownDescription: "The repository to apply the pruning policy on", + Required: true, + }, + }, + + Blocks: map[string]schema.Block{ + "rule": schema.ListNestedBlock{ + MarkdownDescription: "The rules of the pruning policy", + NestedObject: schema.NestedBlockObject{ + + Attributes: map[string]schema.Attribute{ + "field": schema.StringAttribute{ + MarkdownDescription: "The field for the rule", + Required: true, + }, + "operator": schema.StringAttribute{ + MarkdownDescription: "The operator for the particular field", + Required: true, + }, + "values": schema.ListAttribute{ + MarkdownDescription: "The regex values for the rule", + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + } +} + +func (r *PruningPolicyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(client.Client) + if !ok { + resp.Diagnostics.AddError( + "Client error", + fmt.Sprintf("Expected client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *PruningPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "Preparing to create pruning policy resource") + var data PruningPolicyResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + pruningPolicy := client.CreatePruningPolicy{ + Enabled: true, + Rules: client.PruningPolicyRulesToAPI(ctx, data.Rules), + } + + if resp.Diagnostics.HasError() { + return + } + + if r.client.TestMode { + resp.Diagnostics.AddWarning("testing mode warning", "msr repo resource handler is in testing mode, no creation will be run.") + data.Id = basetypes.NewStringValue(TestingVersion) + } else { + rPolicy, err := r.client.CreatePruningPolicy(ctx, data.OrgName.ValueString(), data.RepoName.ValueString(), pruningPolicy) + if err != nil { + resp.Diagnostics.AddError( + "Unexpected Create pruning policy error", + err.Error(), + ) + return + } + + tflog.Trace(ctx, fmt.Sprintf("created Pruning policy resource with ID `%s`", data.Id.ValueString())) + data.Id = basetypes.NewStringValue(rPolicy.ID) + data.Rules = client.PruningPolicyRulesToTFSDK(ctx, rPolicy.Rules) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *PruningPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "Preparing to read pruning policy resource") + var data PruningPolicyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if r.client.TestMode { + resp.Diagnostics.AddWarning("testing mode warning", "msr pruning policy resource handler is in testing mode, no read will be run.") + data.Id = types.StringValue(TestingVersion) + } else { + rPolicy, err := r.client.ReadPruningPolicy(ctx, data.OrgName.ValueString(), data.RepoName.ValueString(), data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", err.Error()) + return + } + data.Id = types.StringValue(rPolicy.ID) + data.Enabled = basetypes.NewBoolValue(rPolicy.Enabled) + data.Rules = client.PruningPolicyRulesToTFSDK(ctx, rPolicy.Rules) + + } + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *PruningPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Debug(ctx, "Preparing to update pruning policy resource") + + var data PruningPolicyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if r.client.TestMode { + resp.Diagnostics.AddWarning("testing mode warning", "msr pruning policy resource handler is in testing mode, no update will be run.") + data.Id = types.StringValue(TestingVersion) + } else { + policy := client.CreatePruningPolicy{ + Enabled: data.Enabled.ValueBool(), + Rules: client.PruningPolicyRulesToAPI(ctx, data.Rules), + } + rPolicy, err := r.client.UpdatePruningPolicy(ctx, data.OrgName.ValueString(), data.RepoName.ValueString(), policy, data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", err.Error()) + return + } + + // Overwrite pruning policy with refreshed state + data.Id = types.StringValue(rPolicy.ID) + data.Enabled = types.BoolValue(rPolicy.Enabled) + data.Rules = client.PruningPolicyRulesToTFSDK(ctx, rPolicy.Rules) + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + // Set refreshed state + tflog.Debug(ctx, fmt.Sprintf("Updated Pruning Policy with ID %s of for %s/%s ", data.Id, data.OrgName, data.RepoName), map[string]any{"success": true}) +} + +func (r *PruningPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Debug(ctx, "Preparing to delete pruning policy resource") + var data *PruningPolicyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if r.client.TestMode { + resp.Diagnostics.AddWarning("testing mode warning", "msr pruning policy resource handler is in testing mode, no deletion will be run.") + } else if err := r.client.DeletePruningPolicy(ctx, data.OrgName.ValueString(), data.RepoName.ValueString(), data.Id.ValueString()); err != nil { + resp.Diagnostics.AddError("Client Error", err.Error()) + return + } + + tflog.Debug(ctx, "Deleted pruning policy resource", map[string]any{"success": true}) +} + +func (r *PruningPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + + idParts := strings.Split(req.ID, ",") + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: org_name,repo_name,pruning_policy_id. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("org_name"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("repo_name"), idParts[1])...) + // policy ID + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[2])...) +} diff --git a/internal/provider/pruning_policy_resource_test.go b/internal/provider/pruning_policy_resource_test.go new file mode 100644 index 0000000..2cac885 --- /dev/null +++ b/internal/provider/pruning_policy_resource_test.go @@ -0,0 +1,82 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestPruningPolicyResourceDefault(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: providerConfig + testPruningPolicyResourceDefault(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("msr_pruning_policy.test", "enabled", "true"), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "org_name", TestingVersion), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "repo_name", TestingVersion), + // first rule + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.field", TestingVersion), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.operator", TestingVersion), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.values.0", TestingVersion), + // second rule + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.1.field", TestingVersion), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.1.operator", TestingVersion), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.1.values.0", TestingVersion), + // Verify placeholder id attribute + resource.TestCheckResourceAttrSet("msr_pruning_policy.test", "id"), + ), + }, + // ImportState testing + { + ResourceName: "msr_pruning_policy.test", + ImportState: true, + ImportStateId: "test,test,test", + }, + // Update and Read testing + { + Config: providerConfig + ` + resource "msr_pruning_policy" "test" { + enabled = "false" + org_name = "blah" + repo_name = "blah" + rule { + field = "blah" + operator = "blah" + values = ["blah"] + } + }`, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("msr_pruning_policy.test", "enabled", "false"), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "org_name", "blah"), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "repo_name", "blah"), + // first rule + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.field", "blah"), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.operator", "blah"), + resource.TestCheckResourceAttr("msr_pruning_policy.test", "rule.0.values.0", "blah"), + ), + }, + // Delete is called implicitly + }, + }) +} + +func testPruningPolicyResourceDefault() string { + return ` + resource "msr_pruning_policy" "test" { + enabled = "true" + org_name = "test" + repo_name = "test" + rule { + field = "test" + operator = "test" + values = ["test"] + } + rule { + field = "test" + operator = "test" + values = ["test"] + } + }` +}