Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Dec 3, 2024
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
25 changes: 25 additions & 0 deletions client/msp/tenants/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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("Creating tenant for CDO")
siddhuwarrier marked this conversation as resolved.
Show resolved Hide resolved
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}
}

return &createOutp.MspManagedTenant, nil
}

type StatusInfo struct {
siddhuwarrier marked this conversation as resolved.
Show resolved Hide resolved
status string
}
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
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")
}
}
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
46 changes: 37 additions & 9 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 @@ -33,17 +35,22 @@ func (*TenantResource) Schema(ctx context.Context, request resource.SchemaReques
},
"name": schema.StringAttribute{
MarkdownDescription: "Name of the tenant",
Required: true,
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.",
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",
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
58 changes: 58 additions & 0 deletions provider/validators/msp_tenant_name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package validators

import (
"context"
"regexp"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

var _ validator.String = mspManagedTenantNameValidator{}

var nameRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]{1,50}$`)

type mspManagedTenantNameValidator struct {
}

func (v mspManagedTenantNameValidator) Description(ctx context.Context) string {
return "Ensures that if name is null and api_token is null, fail. If name is not null and api_token is not null, fail. If name is not null and does not match the regex [a-zA-Z0-9-_]{1,50}, fail."
}

func (v mspManagedTenantNameValidator) MarkdownDescription(ctx context.Context) string {
return "Ensures that if name is null and api_token is null, fail. If name is not null and api_token is not null, fail. If name is not null and does not match the regex [a-zA-Z0-9-_]{1,50}, fail."
}

func (v mspManagedTenantNameValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) {
var apiTokenAttr attr.Value

request.Config.GetAttribute(ctx, path.Root("api_token"), &apiTokenAttr)

if request.ConfigValue.IsNull() && apiTokenAttr.IsNull() {
response.Diagnostics.AddError(
"Invalid Configuration",
"Both name and api_token cannot be null.",
)
return
}

if !request.ConfigValue.IsNull() && !apiTokenAttr.IsNull() {
response.Diagnostics.AddError(
"Invalid Configuration",
"Both name and api_token cannot be specified at the same time.",
)
return
}

if !request.ConfigValue.IsNull() && !nameRegex.MatchString(request.ConfigValue.ValueString()) {
response.Diagnostics.AddError(
"Invalid Configuration",
"Name must match the regex `[a-zA-Z0-9-_]{1,50}`.",
)
}
}

func NewMspManagedTenantNameValidator() validator.String {
return mspManagedTenantNameValidator{}
}
84 changes: 84 additions & 0 deletions provider/validators/msp_tenant_name_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package validators_test

import (
"context"
"github.com/CiscoDevnet/terraform-provider-cdo/validators"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"testing"
)

func TestMspTenantNameValidator(t *testing.T) {
t.Parallel()

type testCase struct {
name types.String
apiToken attr.Value
expectError bool
}

testCases := map[string]testCase{
"non-null-name-and-non-null-api-token": {
name: types.StringValue("burak-crush-pineapple"),
apiToken: types.StringValue("burak-crush-api-token"),
expectError: true,
},
"non-null-name-and-null-api-token": {
name: types.StringValue("burak-crush-pineapple"),
apiToken: nil,
expectError: false,
},
"null-name-and-null-api-token": {
name: types.StringNull(),
apiToken: nil,
expectError: true,
},
"null-name-and-non-null-api-token": {
name: types.StringNull(),
apiToken: types.StringValue("burak-crush-api-token"),
expectError: false,
},
}

for name, test := range testCases {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
req := validator.StringRequest{ // nolint
ConfigValue: test.name,
Config: tfsdk.Config{
Raw: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"api_token": tftypes.String,
},
}, map[string]tftypes.Value{
"api_token": tftypes.NewValue(tftypes.String, test.apiToken.(types.String).ValueString()), // nolint
}),
Schema: schema.Schema{
Attributes: map[string]schema.Attribute{
"api_token": schema.StringAttribute{
MarkdownDescription: "API token for an API-only user with super-admin privileges on the tenant",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(), // Prevent updates to name
},
},
},
}, // Provide the actual schema if needed
},
}
res := validator.StringResponse{}
validators.NewMspManagedTenantNameValidator().ValidateString(context.TODO(), req, &res)
if test.expectError && !res.Diagnostics.HasError() {
t.Fatalf("expected error, got none")

}
})
}
}
Loading