Skip to content

Commit

Permalink
feat(lh-86970): add Terraform resource to add an existing tenant to M…
Browse files Browse the repository at this point in the history
…SP portal using API token (#153)

* feat(lh-86970): add Terraform resource to add an existing tenant to MSP portal using API token

* fix(lh-86970): address Tal's comments and improve documentation

* test(lh-86970): add more tests, make acceptance tests run temporarily on branch

* revert this before merging

* fix(lh-86970): add ADDED_MSP_MANAGED_TENANT_API_TOKEN to the env for acceptance tests

* I hate Github

* Stop running acceptance tests on branch
  • Loading branch information
siddhuwarrier authored Dec 3, 2024
1 parent 688b711 commit 4364675
Show file tree
Hide file tree
Showing 19 changed files with 376 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ jobs:
ASA_RESOURCE_SDC_PASSWORD: ${{ secrets.ASA_RESOURCE_SDC_PASSWORD }}
DUO_ADMIN_PANEL_RESOURCE_INTEGRATION_KEY: ${{ secrets.DUO_ADMIN_PANEL_RESOURCE_INTEGRATION_KEY }}
DUO_ADMIN_PANEL_RESOURCE_SECRET_KEY: ${{ secrets.DUO_ADMIN_PANEL_RESOURCE_SECRET_KEY }}
ADDED_MSP_MANAGED_TENANT_API_TOKEN: ${{ secrets.ADDED_MSP_MANAGED_TENANT_API_TOKEN }}
run: go test -v -cover -p 1 -run "TestAcc.*" ./...
timeout-minutes: 10
tag-release-version:
Expand Down
4 changes: 4 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ func (c *Client) CreateTenantUsingMspPortal(ctx context.Context, createInput ten
return tenants.Create(ctx, c.client, createInput)
}

func (c *Client) AddExistingTenantToMspPortalUsingApiToken(ctx context.Context, createInput tenants.MspAddExistingTenantInput) (*tenants.MspTenantOutput, *tenants.CreateError) {
return tenants.AddExistingTenantUsingApiToken(ctx, c.client, createInput)
}

func (c *Client) ReadMspManagedTenantByUid(ctx context.Context, readByUidInput tenants.ReadByUidInput) (*tenants.MspTenantOutput, error) {
return tenants.ReadByUid(ctx, c.client, readByUidInput)
}
Expand Down
4 changes: 4 additions & 0 deletions client/internal/url/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ func CreateMspManagedTenant(baseUrl string) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants/create", baseUrl)
}

func AddExistingTenantToMspManagedTenant(baseUrl string) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants", baseUrl)
}

func MspManagedTenantByUid(baseUrl string, tenantUid string) string {
return fmt.Sprintf("%s/api/rest/v1/msp/tenants/%s", baseUrl, tenantUid)
}
Expand Down
23 changes: 23 additions & 0 deletions client/msp/tenants/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package tenants

import (
"context"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url"
)

func AddExistingTenantUsingApiToken(ctx context.Context, client http.Client, addInp MspAddExistingTenantInput) (*MspTenantOutput, *CreateError) {
client.Logger.Println("Adding existing tenant to MSp portal...")
addUrl := url.AddExistingTenantToMspManagedTenant(client.BaseUrl())

req := client.NewPost(ctx, addUrl, addInp)

var createOutp MspManagedTenantStatusInfo
if err := req.Send(&createOutp); err != nil {
return nil, &CreateError{Err: err}
}

client.Logger.Printf("Added existing tenant %s to MSP portal using API token...", createOutp.MspManagedTenant.Name)

return &createOutp.MspManagedTenant, nil
}
66 changes: 66 additions & 0 deletions client/msp/tenants/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tenants_test

import (
"context"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/tenants"
"github.com/google/uuid"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
netHttp "net/http"
"testing"
"time"
)

func TestAdd(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

t.Run("successfully add tenant using API token", func(t *testing.T) {
httpmock.Reset()
apiToken := "fake-jwt-token"
addInp := tenants.MspAddExistingTenantInput{
ApiToken: apiToken,
}
expectedResponse := tenants.MspManagedTenantStatusInfo{
Status: "OK",
MspManagedTenant: tenants.MspTenantOutput{
Uid: uuid.New().String(),
Name: "example-name",
DisplayName: "Human readable name",
Region: "STAGING",
},
}

httpmock.RegisterResponder(
netHttp.MethodPost,
"/api/rest/v1/msp/tenants",
httpmock.NewJsonResponderOrPanic(201, expectedResponse),
)

actual, err := tenants.AddExistingTenantUsingApiToken(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), addInp)

assert.NotNil(t, actual, "Response for added tenant should have not been nil")
assert.Nil(t, err, "Add tenant operation should have not been an error")
assert.Equal(t, expectedResponse.MspManagedTenant, *actual, "Add tenant operation should have returned the value of the added tenant")
})

t.Run("fail to add tenant using API token", func(t *testing.T) {
httpmock.Reset()
apiToken := "fake-jwt-token"
addInp := tenants.MspAddExistingTenantInput{
ApiToken: apiToken,
}

httpmock.RegisterResponder(
netHttp.MethodPost,
"/api/rest/v1/msp/tenants",
httpmock.NewJsonResponderOrPanic(400, nil),
)

actual, err := tenants.AddExistingTenantUsingApiToken(context.Background(), *http.MustNewWithConfig(baseUrl, "valid_token", 0, 0, time.Minute), addInp)
assert.Nil(t, actual, "Response for added tenant should have been nil")
assert.NotNil(t, err, "Add tenant operation should have been an error")
})

}
5 changes: 5 additions & 0 deletions client/msp/tenants/constants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tenants_test

const (
baseUrl = "https://unittest.cdo.cisco.com"
)
6 changes: 1 addition & 5 deletions client/msp/tenants/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import (
"time"
)

const (
baseUrl = "https://unittest.cdo.cisco.com"
)

func TestCreate(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
Expand Down Expand Up @@ -80,7 +76,7 @@ func TestCreate(t *testing.T) {

assert.NotNil(t, actual, "Created tenant should have not been nil")
assert.Nil(t, err, "Created tenant operation should have not been an error")
assert.Equal(t, creationOutput, *actual, "Created tenant operation should have been the same as the created tenant")
assert.Equal(t, creationOutput, *actual, "Create tenant operation should have been the same as the created tenant")
})

t.Run("tenant creation transaction fails", func(t *testing.T) {
Expand Down
9 changes: 9 additions & 0 deletions client/msp/tenants/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ type MspCreateTenantInput struct {
DisplayName string `json:"displayName"`
}

type MspAddExistingTenantInput struct {
ApiToken string `json:"apiToken"`
}

type MspManagedTenantStatusInfo struct {
Status string `json:"uid"`
MspManagedTenant MspTenantOutput `json:"mspManagedTenant"`
}

type MspTenantOutput struct {
Uid string `json:"uid"`
Name string `json:"name"`
Expand Down
8 changes: 3 additions & 5 deletions docs/resources/msp_managed_tenant.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ Provides an MSP managed tenant resource. This allows MSP managed tenants to be c
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Name of the tenant

### Optional

- `display_name` (String) Display name of the tenant. If no display name is specified, the display name will be set to the tenant name.
- `api_token` (String, Sensitive) API token for an API-only user with super-admin privileges on the tenant. This should be specified only when adding an existing tenant to the MSP portal, and should not be provided if a new tenant is being created (i.e., the `name` and/or `display_name` attributes are specified).
- `display_name` (String) Display name of the tenant. If no display name is specified, the display name will be set to the tenant name. This should be specified only if a new tenant is being created, and should not be provided if an existing tenant is being added to the MSP protal (i.e., the `api_token` attribute is specified).
- `name` (String) Name of the tenant. This should be specified only if a new tenant is being created, and should not be provided if an existing tenant is being added to the MSP protal (i.e., the `api_token` attribute is specified).

### Read-Only

Expand Down
3 changes: 3 additions & 0 deletions provider/.github-action.env
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ MSP_TENANT_DISPLAY_NAME=terraform-provider-cdo
MSP_TENANT_REGION=CI
MSP_TENANT_ID=ae98d25f-1089-4286-a3c5-505dcb4431a2
TF_LOG=DEBUG
ADDED_MSP_MANAGED_TENANT_DISPLAY_NAME=Added MSP Managed Tenant
ADDED_MSP_MANAGED_TENANT_NAME=CDO_added-msp-managed-tenant__snx85b
ADDED_MSP_MANAGED_TENANT_UID=67cf9b3e-f06c-46e2-83a9-8a98747fa759
2 changes: 1 addition & 1 deletion provider/examples/resources/msp/tenants/api_token.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Paste your API token here
add API token here
4 changes: 4 additions & 0 deletions provider/examples/resources/msp/tenants/main.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
resource "cdo_msp_managed_tenant" "tenant" {
name = "test-tenant-name"
display_name = "Display name for tenant"
}

resource "cdo_msp_managed_tenant" "existing_tenant" {
api_token = "existing-tenant-api-token"
}
4 changes: 2 additions & 2 deletions provider/examples/resources/msp/tenants/providers.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ terraform {
}

provider "cdo" {
base_url = "<https://www.defenseorchestrator.com|https://www.defenseorchestrator.eu|https://apj.cdo.cisco.com|https://aus.cdo.cisco.com|https://in.cdo.cisco.com>""
base_url = "<https://www.defenseorchestrator.com|https://www.defenseorchestrator.eu|https://apj.cdo.cisco.com|https://aus.cdo.cisco.com|https://in.cdo.cisco.com>"
api_token = file("${path.module}/api_token.txt")
}
}
20 changes: 20 additions & 0 deletions provider/internal/acctest/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,26 @@ func (e *env) MspTenantId() string {
return e.mustGetString("MSP_TENANT_ID")
}

func (e *env) AddedMspManagedTenantId() string {
return e.mustGetString("ADDED_MSP_MANAGED_TENANT_UID")
}

func (e *env) AddedMspManagedTenantName() string {
return e.mustGetString("ADDED_MSP_MANAGED_TENANT_NAME")
}

func (e *env) AddedMspManagedTenantDisplayName() string {
return e.mustGetString("ADDED_MSP_MANAGED_TENANT_DISPLAY_NAME")
}

func (e *env) AddedMspManagedTenantApiToken() string {
return e.mustGetString("ADDED_MSP_MANAGED_TENANT_API_TOKEN")
}

func (e *env) MspManagedTenantRegion() string {
return e.mustGetString("ADDED_MSP_MANAGED_TENANT_REGION")
}

func (e *env) MspTenantRegion() string {
return e.mustGetString("MSP_TENANT_REGION")
}
Expand Down
1 change: 1 addition & 0 deletions provider/internal/msp/msp_tenant/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type TenantResourceModel struct {
DisplayName types.String `tfsdk:"display_name"`
GeneratedName types.String `tfsdk:"generated_name"`
Region types.String `tfsdk:"region"`
ApiToken types.String `tfsdk:"api_token"`
}

type TenantDatasourceModel struct {
Expand Down
50 changes: 39 additions & 11 deletions provider/internal/msp/msp_tenant/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client"
"github.com/CiscoDevnet/terraform-provider-cdo/go-client/msp/tenants"
"github.com/CiscoDevnet/terraform-provider-cdo/internal/util"
"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
Expand All @@ -32,18 +34,23 @@ func (*TenantResource) Schema(ctx context.Context, request resource.SchemaReques
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "Name of the tenant",
Required: true,
MarkdownDescription: "Name of the tenant. This should be specified only if a new tenant is being created, and should not be provided if an existing tenant is being added to the MSP protal (i.e., the `api_token` attribute is specified).",
Optional: true,
PlanModifiers: []planmodifier.String{
PreventUpdatePlanModifier{}, // Prevent updates to name
},
Validators: []validator.String{
validators.NewMspManagedTenantNameValidator(),
},
Computed: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Display name of the tenant. If no display name is specified, the display name will be set to the tenant name.",
MarkdownDescription: "Display name of the tenant. If no display name is specified, the display name will be set to the tenant name. This should be specified only if a new tenant is being created, and should not be provided if an existing tenant is being added to the MSP protal (i.e., the `api_token` attribute is specified).",
Optional: true,
PlanModifiers: []planmodifier.String{
PreventUpdatePlanModifier{}, // Prevent updates to name
},
Computed: true,
},
"generated_name": schema.StringAttribute{
MarkdownDescription: "Actual name of the tenant returned by the API. This auto-generated name will differ from the name entered by the customer.",
Expand All @@ -53,6 +60,14 @@ func (*TenantResource) Schema(ctx context.Context, request resource.SchemaReques
MarkdownDescription: "CDO region in which the tenant is created. This is the same region as the region of the MSP portal.",
Computed: true,
},
"api_token": schema.StringAttribute{
MarkdownDescription: "API token for an API-only user with super-admin privileges on the tenant. This should be specified only when adding an existing tenant to the MSP portal, and should not be provided if a new tenant is being created (i.e., the `name` and/or `display_name` attributes are specified).",
Optional: true,
PlanModifiers: []planmodifier.String{
PreventUpdatePlanModifier{}, // Prevent updates to api token
},
Sensitive: true,
},
},
}
}
Expand All @@ -77,7 +92,7 @@ func (t *TenantResource) Configure(ctx context.Context, req resource.ConfigureRe
}

func (t *TenantResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
tflog.Debug(ctx, "Creating a CDO tenant")
tflog.Debug(ctx, "Creating a CDO tenant/Adding an existing tenant using API token to the MSP portal...")

// 1. Read plan data into planData
var planData TenantResourceModel
Expand All @@ -88,18 +103,31 @@ func (t *TenantResource) Create(ctx context.Context, request resource.CreateRequ
return
}

// 2. use plan data to create tenant and fill up rest of the model
createOut, err := t.client.CreateTenantUsingMspPortal(ctx, tenants.MspCreateTenantInput{
Name: planData.Name.ValueString(),
DisplayName: planData.DisplayName.ValueString(),
})
if err != nil {
var createOut *tenants.MspTenantOutput
var err *tenants.CreateError
if !planData.ApiToken.IsNull() {
// add tenant to MSP portal
tflog.Debug(ctx, "Adding existing tenant using API token to MSP portal")
createOut, err = t.client.AddExistingTenantToMspPortalUsingApiToken(ctx, tenants.MspAddExistingTenantInput{ApiToken: planData.ApiToken.ValueString()})
} else {
tflog.Debug(ctx, "Creating new tenant and adding it to the MSP portal")
// 2. use plan data to create tenant and fill up rest of the model
createOut, err = t.client.CreateTenantUsingMspPortal(ctx, tenants.MspCreateTenantInput{
Name: planData.Name.ValueString(),
DisplayName: planData.DisplayName.ValueString(),
})
}

if err != nil || createOut == nil {
response.Diagnostics.AddError("failed to create CDO Tenant", err.Error())
return
}

planData.Id = types.StringValue(createOut.Uid)
planData.Name = types.StringValue(planData.Name.ValueString())
// when a new tenant is created, the name is auto-generated, do not set it to planData.Name
if planData.Name.IsNull() || planData.Name.IsUnknown() {
planData.Name = types.StringValue(createOut.Name)
}
planData.DisplayName = types.StringValue(createOut.DisplayName)
planData.GeneratedName = types.StringValue(createOut.Name)
planData.Region = types.StringValue(createOut.Region)
Expand Down
48 changes: 48 additions & 0 deletions provider/internal/msp/msp_tenant/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package msp_tenant_test

import (
"github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"strings"
"testing"
)

var testMspTenantResource = struct {
Name string
DisplayName string
Id string
Region string
ApiToken string
}{
ApiToken: acctest.Env.AddedMspManagedTenantApiToken(),
Name: acctest.Env.AddedMspManagedTenantName(),
DisplayName: acctest.Env.AddedMspManagedTenantDisplayName(),
Id: acctest.Env.AddedMspManagedTenantId(),
Region: strings.ToUpper(acctest.Env.MspTenantRegion()),
}

const testMspTenantResourceTemplate = `
resource "cdo_msp_managed_tenant" "test" {
api_token = "{{.ApiToken}}"
}`

var testMspTenantResourceConfig = acctest.MustParseTemplate(testMspTenantResourceTemplate, testMspTenantResource)

func TestAccMspTenantResource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: acctest.PreCheckFunc(t),
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing
{
Config: acctest.MspProviderConfig() + testMspTenantResourceConfig,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("cdo_msp_managed_tenant.test", "name", testMspTenantResource.Name),
resource.TestCheckResourceAttr("cdo_msp_managed_tenant.test", "display_name", testMspTenantResource.DisplayName),
resource.TestCheckResourceAttr("cdo_msp_managed_tenant.test", "id", testMspTenantResource.Id),
resource.TestCheckResourceAttr("cdo_msp_managed_tenant.test", "region", testMspTenantResource.Region),
),
},
},
})
}
Loading

0 comments on commit 4364675

Please sign in to comment.