Skip to content

Commit

Permalink
Merge pull request #75 from kaleido-io/v1.1-amspolicy
Browse files Browse the repository at this point in the history
Add AMS Policy
  • Loading branch information
hosie authored Jun 19, 2024
2 parents 5aa05a6 + 60ef52a commit 8568a6b
Show file tree
Hide file tree
Showing 4 changed files with 433 additions and 38 deletions.
213 changes: 213 additions & 0 deletions kaleido/platform/ams_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright © Kaleido, Inc. 2024

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package platform

import (
"context"
"fmt"
"net/http"

"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type AMSPolicyResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Environment types.String `tfsdk:"environment"`
Service types.String `tfsdk:"service"`
Document types.String `tfsdk:"document"` // this is propagated to a policy version
ExampleInput types.String `tfsdk:"example_input"` // this is propagated to a policy version
Hash types.String `tfsdk:"hash"`
AppliedVersion types.String `tfsdk:"applied_version"`
}

type AMSPolicyAPIModel struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Created string `json:"created,omitempty"`
Updated string `json:"updated,omitempty"`
CurrentVersion string `json:"currentVersion,omitempty"`
}

type AMSPolicyVersionAPIModel struct {
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
Document string `json:"document,omitempty"`
ExampleInput string `json:"exampleInput,omitempty"`
Hash string `json:"hash,omitempty"`
Created string `json:"created,omitempty"`
Updated string `json:"updated,omitempty"`
}

func AMSPolicyResourceFactory() resource.Resource {
return &ams_policyResource{}
}

type ams_policyResource struct {
commonResource
}

func (r *ams_policyResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "kaleido_platform_ams_policy"
}

func (r *ams_policyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": &schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"environment": &schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"name": &schema.StringAttribute{
Required: true,
},
"description": &schema.StringAttribute{
Optional: true,
},
"service": &schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"document": &schema.StringAttribute{
Required: true,
Description: "This is the definition of the policy - which will be put into a new version each time the policy is updated",
},
"example_input": &schema.StringAttribute{
Optional: true,
},
"hash": &schema.StringAttribute{
Computed: true,
},
"applied_version": &schema.StringAttribute{
Computed: true,
},
},
}
}

func (api *AMSPolicyAPIModel) toData(data *AMSPolicyResourceModel) {
data.ID = types.StringValue(api.ID)
data.AppliedVersion = types.StringValue(api.CurrentVersion)
}

func (api *AMSPolicyVersionAPIModel) toData(data *AMSPolicyResourceModel) {
data.Hash = types.StringValue(api.Hash)
}

func (data *AMSPolicyResourceModel) toAPI(api *AMSPolicyAPIModel, apiV *AMSPolicyVersionAPIModel) {
api.Name = data.Name.ValueString()
api.Description = data.Description.ValueString()

apiV.Description = data.Description.ValueString()
apiV.Document = data.Document.ValueString()
if data.ExampleInput.ValueString() != "" {
apiV.ExampleInput = data.ExampleInput.ValueString()
}
}

func (r *ams_policyResource) apiPath(data *AMSPolicyResourceModel, idOrName string) string {
return fmt.Sprintf("/endpoint/%s/%s/rest/api/v1/policies/%s", data.Environment.ValueString(), data.Service.ValueString(), idOrName)
}

func (r *ams_policyResource) apiPolicyVersionPath(data *AMSPolicyResourceModel, idOrName string) string {
return fmt.Sprintf("%s/versions", r.apiPath(data, idOrName))
}

func (r *ams_policyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {

var data AMSPolicyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

var api AMSPolicyAPIModel
var apiV AMSPolicyVersionAPIModel
data.toAPI(&api, &apiV)
// Policy PUT
ok, _ := r.apiRequest(ctx, http.MethodPut, r.apiPath(&data, api.Name), &api, &api, &resp.Diagnostics)
if ok {
// Policy version POST
ok, _ = r.apiRequest(ctx, http.MethodPost, r.apiPolicyVersionPath(&data, api.Name), &apiV, &apiV, &resp.Diagnostics)
}
if !ok {
return
}

api.toData(&data)
apiV.toData(&data)
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)

}

func (r *ams_policyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {

var data AMSPolicyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &data.ID)...)

var api AMSPolicyAPIModel
var apiV AMSPolicyVersionAPIModel
data.toAPI(&api, &apiV)
policyID := data.ID.ValueString()
// Policy PATCH
ok, _ := r.apiRequest(ctx, http.MethodPatch, r.apiPath(&data, policyID), &api, &api, &resp.Diagnostics)
if ok {
// Policy version POST
ok, _ = r.apiRequest(ctx, http.MethodPost, r.apiPolicyVersionPath(&data, api.Name), &apiV, &apiV, &resp.Diagnostics)
}
if !ok {
return
}

api.toData(&data)
apiV.toData(&data)
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}

func (r *ams_policyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data AMSPolicyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

var api AMSPolicyAPIModel
api.ID = data.ID.ValueString()
ok, status := r.apiRequest(ctx, http.MethodGet, r.apiPath(&data, data.ID.ValueString()), nil, &api, &resp.Diagnostics, Allow404())
if !ok {
return
}
if status == 404 {
resp.State.RemoveResource(ctx)
return
}

api.toData(&data)
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}

func (r *ams_policyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data AMSPolicyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

_, _ = r.apiRequest(ctx, http.MethodDelete, r.apiPath(&data, data.ID.ValueString()), nil, nil, &resp.Diagnostics, Allow404())

r.waitForRemoval(ctx, r.apiPath(&data, data.ID.ValueString()), &resp.Diagnostics)
}
170 changes: 170 additions & 0 deletions kaleido/platform/ams_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright © Kaleido, Inc. 2024

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package platform

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"testing"
"time"

"github.com/aidarkhanov/nanoid"
"github.com/gorilla/mux"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/stretchr/testify/assert"

_ "embed"
)

var ams_policyStep1 = `
resource "kaleido_platform_ams_policy" "ams_policy1" {
environment = "env1"
service = "service1"
name = "ams_policy1"
document = "document 1"
}
`

var ams_policyStep2 = `
resource "kaleido_platform_ams_policy" "ams_policy1" {
environment = "env1"
service = "service1"
name = "ams_policy1"
description = "shiny policy that does stuff and more stuff"
document = "document 2"
example_input = "input 2"
}
`

func TestAMSPolicy1(t *testing.T) {

mp, providerConfig := testSetup(t)
defer func() {
mp.checkClearCalls([]string{
"PUT /endpoint/{env}/{service}/rest/api/v1/policies/{policy}", // by name initially
"POST /endpoint/{env}/{service}/rest/api/v1/policies/{policy}/versions",
"GET /endpoint/{env}/{service}/rest/api/v1/policies/{policy}",
"GET /endpoint/{env}/{service}/rest/api/v1/policies/{policy}",
"PATCH /endpoint/{env}/{service}/rest/api/v1/policies/{policy}", // then by ID
"POST /endpoint/{env}/{service}/rest/api/v1/policies/{policy}/versions",
"GET /endpoint/{env}/{service}/rest/api/v1/policies/{policy}",
"DELETE /endpoint/{env}/{service}/rest/api/v1/policies/{policy}",
"GET /endpoint/{env}/{service}/rest/api/v1/policies/{policy}",
})
mp.server.Close()
}()

ams_policy1Resource := "kaleido_platform_ams_policy.ams_policy1"
resource.Test(t, resource.TestCase{
IsUnitTest: true,
ProtoV6ProviderFactories: testAccProviders,
Steps: []resource.TestStep{
{
Config: providerConfig + ams_policyStep1,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(ams_policy1Resource, "id"),
resource.TestCheckResourceAttrSet(ams_policy1Resource, "hash"),
resource.TestCheckResourceAttrSet(ams_policy1Resource, "applied_version"),
),
},
{
Config: providerConfig + ams_policyStep2,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(ams_policy1Resource, "id"),
resource.TestCheckResourceAttrSet(ams_policy1Resource, "hash"),
resource.TestCheckResourceAttrSet(ams_policy1Resource, "applied_version"),
func(s *terraform.State) error {
// Compare the final result on the mock-server side
id := s.RootModule().Resources[ams_policy1Resource].Primary.Attributes["id"]
obj := mp.amsPolicies[fmt.Sprintf("env1/service1/%s", id)]
testJSONEqual(t, obj, fmt.Sprintf(`{
"id": "%[1]s",
"name": "ams_policy1",
"description": "shiny policy that does stuff and more stuff",
"created": "%[2]s",
"updated": "%[3]s",
"currentVersion": "%[4]s"
}`,
// generated fields that vary per test run
id,
obj.Created,
obj.Updated,
obj.CurrentVersion,
))
return nil
},
),
},
},
})
}

func (mp *mockPlatform) getAMSPolicy(res http.ResponseWriter, req *http.Request) {
obj := mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"]]
if obj == nil {
mp.respond(res, nil, 404)
} else {
mp.respond(res, obj, 200)
}
}

func (mp *mockPlatform) putAMSPolicy(res http.ResponseWriter, req *http.Request) {
now := time.Now().UTC()
obj := mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"]] // expected behavior of provider is PUT only on exists
var newObj AMSPolicyAPIModel
mp.getBody(req, &newObj)
assert.Nil(mp.t, obj)
assert.Equal(mp.t, newObj.Name, mux.Vars(req)["policy"])
newObj.ID = nanoid.New()
newObj.Created = now.Format(time.RFC3339Nano)
newObj.Updated = now.Format(time.RFC3339Nano)
newObj.CurrentVersion = nanoid.New()
mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+newObj.ID] = &newObj
mp.respond(res, &newObj, 200)
}

func (mp *mockPlatform) patchAMSPolicy(res http.ResponseWriter, req *http.Request) {
now := time.Now().UTC()
obj := mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"]] // expected behavior of provider is PUT only on exists
var newObj AMSPolicyAPIModel
mp.getBody(req, &newObj)
assert.NotNil(mp.t, obj)
assert.Equal(mp.t, obj.ID, mux.Vars(req)["policy"])
newObj.ID = mux.Vars(req)["policy"]
newObj.Created = obj.Created
newObj.Updated = now.Format(time.RFC3339Nano)
newObj.CurrentVersion = nanoid.New()
mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+newObj.ID] = &newObj
mp.respond(res, &newObj, 200)
}

func (mp *mockPlatform) postAMSPolicyVersion(res http.ResponseWriter, req *http.Request) {
var newObj AMSPolicyVersionAPIModel
mp.getBody(req, &newObj)
mp.amsPolicyVersions[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"]] = &newObj
hash := sha256.New()
hash.Write([]byte(newObj.Document))
newObj.Hash = hex.EncodeToString(hash.Sum(nil))
mp.respond(res, &newObj, 200)
}

func (mp *mockPlatform) deleteAMSPolicy(res http.ResponseWriter, req *http.Request) {
obj := mp.amsPolicies[mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"]]
assert.NotNil(mp.t, obj)
delete(mp.amsPolicies, mux.Vars(req)["env"]+"/"+mux.Vars(req)["service"]+"/"+mux.Vars(req)["policy"])
mp.respond(res, nil, 204)
}
1 change: 1 addition & 0 deletions kaleido/platform/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ func Resources() []func() resource.Resource {
CMSActionDeployResourceFactory,
CMSActionCreateAPIResourceFactory,
AMSTaskResourceFactory,
AMSPolicyResourceFactory,
AMSFFListenerResourceFactory,
AMSDMListenerResourceFactory,
AMSDMUpsertResourceFactory,
Expand Down
Loading

0 comments on commit 8568a6b

Please sign in to comment.