diff --git a/internal/provider/acctest/resource_setting_mgmt_test.go b/internal/provider/acctest/resource_setting_mgmt_test.go index 107b798..c7d0bca 100644 --- a/internal/provider/acctest/resource_setting_mgmt_test.go +++ b/internal/provider/acctest/resource_setting_mgmt_test.go @@ -18,7 +18,7 @@ func TestAccSettingMgmt_basic(t *testing.T) { Config: testAccSettingMgmtConfig_basic(), Check: resource.ComposeTestCheckFunc(), }, - pt.ImportStep("unifi_setting_mgmt.test"), + pt.ImportStepWithSite("unifi_setting_mgmt.test"), }, }) } @@ -31,12 +31,7 @@ func TestAccSettingMgmt_site(t *testing.T) { Config: testAccSettingMgmtConfig_site(), Check: resource.ComposeTestCheckFunc(), }, - { - ResourceName: "unifi_setting_mgmt.test", - ImportState: true, - ImportStateIdFunc: pt.SiteAndIDImportStateIDFunc("unifi_setting_mgmt.test"), - ImportStateVerify: true, - }, + pt.ImportStepWithSite("unifi_setting_mgmt.test"), }, }) } @@ -49,12 +44,7 @@ func TestAccSettingMgmt_sshKeys(t *testing.T) { Config: testAccSettingMgmtConfig_sshKeys(), Check: resource.ComposeTestCheckFunc(), }, - { - ResourceName: "unifi_setting_mgmt.test", - ImportState: true, - ImportStateIdFunc: pt.SiteAndIDImportStateIDFunc("unifi_setting_mgmt.test"), - ImportStateVerify: true, - }, + pt.ImportStepWithSite("unifi_setting_mgmt.test"), }, }) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6102ebe..c8c2f8c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -115,7 +115,6 @@ func New(version string) func() *schema.Provider { "unifi_site": site.ResourceSite(), "unifi_account": radius.ResourceAccount(), "unifi_radius_profile": radius.ResourceRadiusProfile(), - "unifi_setting_mgmt": settings.ResourceSettingMgmt(), "unifi_setting_radius": settings.ResourceSettingRadius(), "unifi_user_group": user.ResourceUserGroup(), "unifi_user": user.ResourceUser(), diff --git a/internal/provider/provider_v2.go b/internal/provider/provider_v2.go index ca8eb38..b337465 100644 --- a/internal/provider/provider_v2.go +++ b/internal/provider/provider_v2.go @@ -186,6 +186,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource settings.NewRsyslogdResource, settings.NewSslInspectionResource, settings.NewTeleportResource, + settings.NewMgmtResource, settings.NewUsgResource, settings.NewUswResource, } diff --git a/internal/provider/settings/resource_setting_mgmt.go b/internal/provider/settings/resource_setting_mgmt.go index 279a248..388efac 100644 --- a/internal/provider/settings/resource_setting_mgmt.go +++ b/internal/provider/settings/resource_setting_mgmt.go @@ -2,21 +2,154 @@ package settings import ( "context" - "errors" "fmt" "github.com/filipowm/go-unifi/unifi" "github.com/filipowm/terraform-provider-unifi/internal/provider/base" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" ) -// TODO: probably need to update this to be more like setting_usg, -// using locking, and upsert, more computed, etc. +// SshKeyModel represents an SSH key configuration +type SshKeyModel struct { + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + Key types.String `tfsdk:"key"` + Comment types.String `tfsdk:"comment"` +} -func ResourceSettingMgmt() *schema.Resource { - return &schema.Resource{ - Description: "The `unifi_setting_mgmt` resource manages site-wide management settings in the UniFi controller.\n\n" + +func (m *SshKeyModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "name": types.StringType, + "type": types.StringType, + "key": types.StringType, + "comment": types.StringType, + } +} + +// mgmtModel represents the data model for management settings. +type mgmtModel struct { + base.Model + AutoUpgrade types.Bool `tfsdk:"auto_upgrade"` + SshEnabled types.Bool `tfsdk:"ssh_enabled"` + SshKeys types.List `tfsdk:"ssh_key"` +} + +func (m *mgmtModel) AsUnifiModel(ctx context.Context) (interface{}, diag.Diagnostics) { + var diags diag.Diagnostics + + sshKeys, d := m.getSshKeys(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + return &unifi.SettingMgmt{ + ID: m.ID.ValueString(), + Key: unifi.SettingMgmtKey, + AutoUpgrade: m.AutoUpgrade.ValueBool(), + XSshEnabled: m.SshEnabled.ValueBool(), + XSshKeys: sshKeys, + }, diags +} + +func (m *mgmtModel) getSshKeys(ctx context.Context) ([]unifi.SettingMgmtXSshKeys, diag.Diagnostics) { + diags := diag.Diagnostics{} + var sshKeys []unifi.SettingMgmtXSshKeys + + if m.SshKeys.IsNull() || m.SshKeys.IsUnknown() { + return sshKeys, diags + } + + var sshKeyElements []SshKeyModel + diags.Append(m.SshKeys.ElementsAs(ctx, &sshKeyElements, false)...) + if diags.HasError() { + return nil, diags + } + + for _, sshKey := range sshKeyElements { + sshKeys = append(sshKeys, unifi.SettingMgmtXSshKeys{ + Name: sshKey.Name.ValueString(), + KeyType: sshKey.Type.ValueString(), + Key: sshKey.Key.ValueString(), + Comment: sshKey.Comment.ValueString(), + }) + } + + return sshKeys, diags +} + +func (m *mgmtModel) Merge(ctx context.Context, other interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + resp, ok := other.(*unifi.SettingMgmt) + if !ok { + diags.AddError("Invalid model type", fmt.Sprintf("Expected *unifi.SettingMgmt, got: %T", other)) + return diags + } + + m.ID = types.StringValue(resp.ID) + m.AutoUpgrade = types.BoolValue(resp.AutoUpgrade) + m.SshEnabled = types.BoolValue(resp.XSshEnabled) + + // Convert SSH keys + if len(resp.XSshKeys) > 0 { + sshKeyElements := make([]SshKeyModel, 0, len(resp.XSshKeys)) + for _, sshKey := range resp.XSshKeys { + sshKeyElements = append(sshKeyElements, SshKeyModel{ + Name: types.StringValue(sshKey.Name), + Type: types.StringValue(sshKey.KeyType), + Key: types.StringValue(sshKey.Key), + Comment: types.StringValue(sshKey.Comment), + }) + } + sshKeys, d := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&SshKeyModel{}).AttributeTypes()}, sshKeyElements) + diags.Append(d...) + if !diags.HasError() { + m.SshKeys = sshKeys + } + } else { + m.SshKeys = types.ListNull(types.ObjectType{AttrTypes: (&SshKeyModel{}).AttributeTypes()}) + } + + return diags +} + +// NewMgmtResource creates a new instance of the management settings resource. +func NewMgmtResource() resource.Resource { + return &mgmtResource{ + GenericResource: NewSettingResource( + "unifi_setting_mgmt", + func() *mgmtModel { return &mgmtModel{} }, + func(ctx context.Context, client *base.Client, site string) (interface{}, error) { + return client.GetSettingMgmt(ctx, site) + }, + func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) { + return client.UpdateSettingMgmt(ctx, site, body.(*unifi.SettingMgmt)) + }, + ), + } +} + +var ( + _ base.ResourceModel = &mgmtModel{} + _ resource.Resource = &mgmtResource{} + _ resource.ResourceWithConfigure = &mgmtResource{} +) + +type mgmtResource struct { + *base.GenericResource[*mgmtModel] +} + +func (r *mgmtResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "The `unifi_setting_mgmt` resource manages site-wide management settings in the UniFi controller.\n\n" + "This resource allows you to configure important management features including:\n" + " * Automatic firmware upgrades for UniFi devices\n" + " * SSH access for advanced configuration and troubleshooting\n" + @@ -26,71 +159,57 @@ func ResourceSettingMgmt() *schema.Resource { " * Maintaining device security through automatic updates\n" + " * Enabling secure remote administration\n" + " * Implementing SSH key-based authentication", - - CreateContext: resourceSettingMgmtCreate, - ReadContext: resourceSettingMgmtRead, - UpdateContext: resourceSettingMgmtUpdate, - DeleteContext: resourceSettingMgmtDelete, - Importer: &schema.ResourceImporter{ - StateContext: base.ImportSiteAndID, - }, - - // TODO add more - Schema: map[string]*schema.Schema{ - "id": { - Description: "The unique identifier of the management settings configuration in the UniFi controller.", - Type: schema.TypeString, - Computed: true, - }, - "site": { - Description: "The name of the UniFi site where these management settings should be applied. If not specified, the default site will be used.", - Type: schema.TypeString, - Computed: true, - Optional: true, - ForceNew: true, - }, - "auto_upgrade": { - Description: "Enable automatic firmware upgrades for all UniFi devices at this site. When enabled, devices will automatically " + + Attributes: map[string]schema.Attribute{ + "id": base.ID(), + "site": base.SiteAttribute(), + "auto_upgrade": schema.BoolAttribute{ + MarkdownDescription: "Enable automatic firmware upgrades for all UniFi devices at this site. When enabled, devices will automatically " + "update to the latest stable firmware version approved for your controller version.", - Type: schema.TypeBool, Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, - "ssh_enabled": { - Description: "Enable SSH access to UniFi devices at this site. When enabled, you can connect to devices using SSH for advanced " + + "ssh_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable SSH access to UniFi devices at this site. When enabled, you can connect to devices using SSH for advanced " + "configuration and troubleshooting. It's recommended to only enable this temporarily when needed.", - Type: schema.TypeBool, Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, - "ssh_key": { - Description: "List of SSH public keys that are allowed to connect to UniFi devices when SSH is enabled. Using SSH keys is more " + + }, + Blocks: map[string]schema.Block{ + "ssh_key": schema.ListNestedBlock{ + MarkdownDescription: "List of SSH public keys that are allowed to connect to UniFi devices when SSH is enabled. Using SSH keys is more " + "secure than password authentication.", - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Description: "A friendly name for the SSH key to help identify its owner or purpose (e.g., 'admin-laptop' or 'backup-server').", - Type: schema.TypeString, - Required: true, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "A friendly name for the SSH key to help identify its owner or purpose (e.g., 'admin-laptop' or 'backup-server').", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, - "type": { - Description: "The type of SSH key. Common values include:\n" + + "type": schema.StringAttribute{ + MarkdownDescription: "The type of SSH key. Common values include:\n" + " * `ssh-rsa` - RSA key (most common)\n" + " * `ssh-ed25519` - Ed25519 key (more secure)\n" + " * `ecdsa-sha2-nistp256` - ECDSA key", - Type: schema.TypeString, Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, - "key": { - Description: "The public key string. This is the content that would normally go in an authorized_keys file, " + + "key": schema.StringAttribute{ + MarkdownDescription: "The public key string. This is the content that would normally go in an authorized_keys file, " + "excluding the type and comment (e.g., 'AAAAB3NzaC1yc2EA...').", - Type: schema.TypeString, Optional: true, }, - "comment": { - Description: "An optional comment to provide additional context about the key (e.g., 'generated on 2024-01-01' or 'expires 2025-12-31').", - Type: schema.TypeString, - Optional: true, + "comment": schema.StringAttribute{ + MarkdownDescription: "An optional comment to provide additional context about the key (e.g., 'generated on 2024-01-01' or 'expires 2025-12-31').", + Optional: true, }, }, }, @@ -98,144 +217,3 @@ func ResourceSettingMgmt() *schema.Resource { }, } } - -func setToSshKeys(set *schema.Set) ([]unifi.SettingMgmtXSshKeys, error) { - var sshKeys []unifi.SettingMgmtXSshKeys - for _, item := range set.List() { - data, ok := item.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("unexpected data in block") - } - sshKey, err := toSshKey(data) - if err != nil { - return nil, fmt.Errorf("unable to create port override: %w", err) - } - sshKeys = append(sshKeys, sshKey) - } - return sshKeys, nil -} - -func toSshKey(data map[string]interface{}) (unifi.SettingMgmtXSshKeys, error) { - return unifi.SettingMgmtXSshKeys{ - Name: data["name"].(string), - KeyType: data["type"].(string), - Key: data["key"].(string), - Comment: data["comment"].(string), - }, nil -} - -func setFromSshKeys(sshKeys []unifi.SettingMgmtXSshKeys) ([]map[string]interface{}, error) { - list := make([]map[string]interface{}, 0, len(sshKeys)) - for _, sshKey := range sshKeys { - v, err := fromSshKey(sshKey) - if err != nil { - return nil, fmt.Errorf("unable to parse ssh key: %w", err) - } - list = append(list, v) - } - return list, nil -} - -func fromSshKey(sshKey unifi.SettingMgmtXSshKeys) (map[string]interface{}, error) { - return map[string]interface{}{ - "name": sshKey.Name, - "type": sshKey.KeyType, - "key": sshKey.Key, - "comment": sshKey.Comment, - }, nil -} - -func resourceSettingMgmtGetResourceData(d *schema.ResourceData, meta interface{}) (*unifi.SettingMgmt, error) { - sshKeys, err := setToSshKeys(d.Get("ssh_key").(*schema.Set)) - if err != nil { - return nil, fmt.Errorf("unable to process ssh_key block: %w", err) - } - - return &unifi.SettingMgmt{ - AutoUpgrade: d.Get("auto_upgrade").(bool), - XSshEnabled: d.Get("ssh_enabled").(bool), - XSshKeys: sshKeys, - }, nil -} - -func resourceSettingMgmtCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*base.Client) - - req, err := resourceSettingMgmtGetResourceData(d, meta) - if err != nil { - return diag.FromErr(err) - } - - site := d.Get("site").(string) - if site == "" { - site = c.Site - } - - resp, err := c.UpdateSettingMgmt(ctx, site, req) - if err != nil { - return diag.FromErr(err) - } - - d.SetId(resp.ID) - - return resourceSettingMgmtSetResourceData(resp, d, meta, site) -} - -func resourceSettingMgmtSetResourceData(resp *unifi.SettingMgmt, d *schema.ResourceData, meta interface{}, site string) diag.Diagnostics { - sshKeys, err := setFromSshKeys(resp.XSshKeys) - if err != nil { - return diag.FromErr(err) - } - - d.Set("site", site) - d.Set("auto_upgrade", resp.AutoUpgrade) - d.Set("ssh_enabled", resp.XSshEnabled) - d.Set("ssh_key", sshKeys) - return nil -} - -func resourceSettingMgmtRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*base.Client) - - site := d.Get("site").(string) - if site == "" { - site = c.Site - } - - resp, err := c.GetSettingMgmt(ctx, site) - if errors.Is(err, unifi.ErrNotFound) { - d.SetId("") - return nil - } - if err != nil { - return diag.FromErr(err) - } - - return resourceSettingMgmtSetResourceData(resp, d, meta, site) -} - -func resourceSettingMgmtUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*base.Client) - - req, err := resourceSettingMgmtGetResourceData(d, meta) - if err != nil { - return diag.FromErr(err) - } - - req.ID = d.Id() - site := d.Get("site").(string) - if site == "" { - site = c.Site - } - - resp, err := c.UpdateSettingMgmt(ctx, site, req) - if err != nil { - return diag.FromErr(err) - } - - return resourceSettingMgmtSetResourceData(resp, d, meta, site) -} - -func resourceSettingMgmtDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return nil -}