diff --git a/edgecenter/lblistener/datasource_lblistener.go b/edgecenter/lblistener/datasource_lblistener.go new file mode 100644 index 00000000..342367df --- /dev/null +++ b/edgecenter/lblistener/datasource_lblistener.go @@ -0,0 +1,142 @@ +package lblistener + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/util" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" +) + +func DataSourceEdgeCenterLbListener() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceEdgeCenterLbListenerRead, + Description: `A listener is a process that checks for connection requests using the protocol and port that you configure.`, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the project", + }, + "region_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the region", + }, + "id": { + Type: schema.TypeString, + Optional: true, + Description: "listener uuid", + ValidateFunc: validation.IsUUID, + ExactlyOneOf: []string{"id", "name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: `listener name. this parameter is not unique, if there is more than one listener with the same name, +then the first one will be used. it is recommended to use "id"`, + ExactlyOneOf: []string{"id", "name"}, + }, + "loadbalancer_id": { + Type: schema.TypeString, + Required: true, + Description: "ID of the load balancer", + }, + // computed attributes + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: "protocol of the load balancer", + }, + "protocol_port": { + Type: schema.TypeInt, + Computed: true, + Description: "protocol port number of the resource", + }, + "secret_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "lifecycle status of the listener", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "operating status of the listener", + }, + "pool_count": { + Type: schema.TypeInt, + Computed: true, + Description: "number of pools", + }, + "insert_headers": { + Type: schema.TypeMap, + Computed: true, + Description: "dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols", + }, + "allowed_cidrs": { + Type: schema.TypeList, + Computed: true, + Description: "allowed CIDRs for listener.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func dataSourceEdgeCenterLbListenerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + loadbalancerID := d.Get("loadbalancer_id").(string) + + var foundListener *edgecloud.Listener + + if id, ok := d.GetOk("id"); ok { + listener, _, err := client.Loadbalancers.ListenerGet(ctx, id.(string)) + if err != nil { + return diag.FromErr(err) + } + + foundListener = listener + } else if listenerName, ok := d.GetOk("name"); ok { + listener, err := util.LBListenerGetByName(ctx, client, listenerName.(string), loadbalancerID) + if err != nil { + return diag.FromErr(err) + } + + foundListener = listener + } else { + return diag.Errorf("Error: specify either id or a name to lookup the listener") + } + + d.SetId(foundListener.ID) + d.Set("name", foundListener.Name) + d.Set("loadbalancer_id", loadbalancerID) + d.Set("provisioning_status", foundListener.ProvisioningStatus) + d.Set("operating_status", foundListener.OperatingStatus) + d.Set("protocol", foundListener.Protocol) + d.Set("protocol_port", foundListener.ProtocolPort) + d.Set("pool_count", foundListener.PoolCount) + d.Set("secret_id", foundListener.SecretID) + + if err := setAllowedCIDRs(ctx, d, foundListener); err != nil { + return diag.FromErr(err) + } + + if err := setInsertHeaders(ctx, d, foundListener); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/edgecenter/lblistener/lblistener.go b/edgecenter/lblistener/lblistener.go new file mode 100644 index 00000000..28e7ee21 --- /dev/null +++ b/edgecenter/lblistener/lblistener.go @@ -0,0 +1,108 @@ +package lblistener + +import ( + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func lblistenerSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the project", + }, + "region_id": { + Type: schema.TypeInt, + Required: true, + Description: "uuid of the region", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: `listener name`, + }, + "loadbalancer_id": { + Type: schema.TypeString, + Required: true, + Description: "ID of the load balancer", + ValidateFunc: validation.IsUUID, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'Terminated HTTPS'", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch edgecloud.LoadbalancerListenerProtocol(v) { + case edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, edgecloud.ListenerProtocolTCP, + edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolTerminatedHTTPS: + return diag.Diagnostics{} + default: + return diag.Errorf("wrong protocol %s, available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'Terminated HTTPS'", v) + } + }, + }, + "protocol_port": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "port on which the protocol is bound", + }, + "insert_x_forwarded": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Description: "add headers X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto to requests. only used with HTTP or TERMINATED_HTTPS protocols", + }, + "secret_id": { + Type: schema.TypeString, + Optional: true, + Description: "ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer", + ValidateFunc: validation.IsUUID, + }, + "sni_secret_id": { + Type: schema.TypeList, + Optional: true, + Description: "list of secret identifiers used for Server Name Indication (SNI).", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "allowed_cidrs": { + Type: schema.TypeList, + Optional: true, + Description: "the allowed CIDRs for listener", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + // computed attributes + "id": { + Type: schema.TypeString, + Computed: true, + Description: "listener uuid", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "operating status of the listener", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "lifecycle status of the listener", + }, + "pool_count": { + Type: schema.TypeInt, + Computed: true, + Description: "number of pools", + }, + "insert_headers": { + Type: schema.TypeMap, + Computed: true, + Description: "dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols", + }, + } +} diff --git a/edgecenter/lblistener/resource_lblistener.go b/edgecenter/lblistener/resource_lblistener.go new file mode 100644 index 00000000..e47caa3c --- /dev/null +++ b/edgecenter/lblistener/resource_lblistener.go @@ -0,0 +1,232 @@ +package lblistener + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/util" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" +) + +func ResourceEdgeCenterLbListener() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceEdgeCenterLbListenerCreate, + ReadContext: resourceEdgeCenterLbListenerRead, + UpdateContext: resourceEdgeCenterLbListenerUpdate, + DeleteContext: resourceEdgeCenterLbListenerDelete, + Description: `A listener is a process that checks for connection requests using the protocol and port that you configure. +Can not be created without a load balancer.`, + Schema: lblistenerSchema(), + + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + protocol := edgecloud.LoadbalancerListenerProtocol(diff.Get("protocol").(string)) + + if diff.HasChange("secret_id") { + if protocol != edgecloud.ListenerProtocolTerminatedHTTPS { + return fmt.Errorf( + "secret_id parameter can only be used with %s listener protocol type", + edgecloud.ListenerProtocolTerminatedHTTPS, + ) + } + } + + if diff.HasChange("sni_secret_id") { + if protocol != edgecloud.ListenerProtocolTerminatedHTTPS { + return fmt.Errorf( + "sni_secret_id parameter can only be used with %s listener protocol type", + edgecloud.ListenerProtocolTerminatedHTTPS, + ) + } + } + + return nil + }, + } +} + +func resourceEdgeCenterLbListenerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + opts := &edgecloud.ListenerCreateRequest{ + Name: d.Get("name").(string), + LoadbalancerID: d.Get("loadbalancer_id").(string), + Protocol: edgecloud.LoadbalancerListenerProtocol(d.Get("protocol").(string)), + ProtocolPort: d.Get("protocol_port").(int), + InsertXForwarded: d.Get("insert_x_forwarded").(bool), + } + + secretID := d.Get("secret_id").(string) + sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) + + switch opts.Protocol { + case edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS: + if secretID != "" { + return diag.Errorf("secret_id parameter can only be used with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) + } + + if len(sniSecretIDRaw) > 0 { + return diag.Errorf("sni_secret_id parameter can only be used with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) + } + + if opts.InsertXForwarded && (opts.Protocol == edgecloud.ListenerProtocolTCP || opts.Protocol == edgecloud.ListenerProtocolUDP || opts.Protocol == edgecloud.ListenerProtocolHTTPS) { + return diag.Errorf( + "X-Forwarded headers can only be used with %s or %s listener protocol type", + edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolTerminatedHTTPS, + ) + } + case edgecloud.ListenerProtocolTerminatedHTTPS: + if secretID == "" { + return diag.Errorf("secret_id parameter is required with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) + } + opts.SecretID = secretID + if len(sniSecretIDRaw) > 0 { + opts.SNISecretID = make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + opts.SNISecretID[i] = s.(string) + } + } + default: + return diag.Errorf("wrong protocol") + } + + allowedCIRDsRaw := d.Get("allowed_cidrs").([]interface{}) + if len(allowedCIRDsRaw) > 0 { + opts.AllowedCIDRs = make([]string, len(allowedCIRDsRaw)) + for i, s := range allowedCIRDsRaw { + opts.AllowedCIDRs[i] = s.(string) + } + } + + log.Printf("[DEBUG] Loadbalancer listener create configuration: %#v", opts) + + taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Loadbalancers.ListenerCreate, opts, client) + if err != nil { + return diag.Errorf("error creating loadbalancer listener: %s", err) + } + + d.SetId(taskResult.Listeners[0]) + + log.Printf("[INFO] Listener: %s", d.Id()) + + return resourceEdgeCenterLbListenerRead(ctx, d, meta) +} + +func resourceEdgeCenterLbListenerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + // Retrieve the loadbalancer listener properties for updating the state + listener, resp, err := client.Loadbalancers.ListenerGet(ctx, d.Id()) + if err != nil { + // check if the loadbalancer listener no longer exists. + if resp != nil && resp.StatusCode == 404 { + log.Printf("[WARN] EdgeCenter Listener (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return diag.Errorf("Error retrieving loadbalancer listener: %s", err) + } + + d.Set("name", listener.Name) + d.Set("protocol", listener.Protocol) + d.Set("protocol_port", listener.ProtocolPort) + d.Set("secret_id", listener.SecretID) + d.Set("sni_secret_id", listener.SNISecretID) + d.Set("operating_status", listener.OperatingStatus) + d.Set("provisioning_status", listener.ProvisioningStatus) + d.Set("pool_count", listener.PoolCount) + + if err := setAllowedCIDRs(ctx, d, listener); err != nil { + return diag.FromErr(err) + } + + if err := setInsertHeaders(ctx, d, listener); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceEdgeCenterLbListenerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + var changed bool + opts := &edgecloud.ListenerUpdateRequest{Name: d.Get("name").(string)} + + if d.HasChange("name") { + changed = true + } + + if d.HasChange("secret_id") { + opts.SecretID = d.Get("secret_id").(string) + changed = true + } + + if d.HasChange("sni_secret_id") { + sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) + sniSecretID := make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + sniSecretID[i] = s.(string) + } + opts.SNISecretID = sniSecretID + changed = true + } + + if d.HasChange("allowed_cidrs") { + allowedCIDRsRaw := d.Get("allowed_cidrs").([]interface{}) + allowedCIDRs := make([]string, len(allowedCIDRsRaw)) + for i, s := range allowedCIDRsRaw { + allowedCIDRs[i] = s.(string) + } + opts.AllowedCIDRs = allowedCIDRs + changed = true + } + + if changed { + task, _, err := client.Loadbalancers.ListenerUpdate(ctx, d.Id(), opts) + if err != nil { + return diag.Errorf("Error when changing the loadbalancer listener: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for loadbalancer listener: %s", err) + } + } + + return resourceEdgeCenterLbListenerRead(ctx, d, meta) +} + +func resourceEdgeCenterLbListenerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + log.Printf("[INFO] Deleting loadbalancer listener: %s", d.Id()) + task, _, err := client.Loadbalancers.ListenerDelete(ctx, d.Id()) + if err != nil { + return diag.Errorf("Error deleting loadbalancer listener: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Delete loadbalancer listener task failed with error: %s", err) + } + + if err = util.ResourceIsDeleted(ctx, client.Loadbalancers.ListenerGet, d.Id()); err != nil { + return diag.Errorf("Loadbalancer listener with id %s was not deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} diff --git a/edgecenter/lblistener/set.go b/edgecenter/lblistener/set.go new file mode 100644 index 00000000..2378ecd5 --- /dev/null +++ b/edgecenter/lblistener/set.go @@ -0,0 +1,31 @@ +package lblistener + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func setAllowedCIDRs(_ context.Context, d *schema.ResourceData, listener *edgecloud.Listener) error { + if len(listener.AllowedCIDRs) > 0 { + allowedCIDRs := make([]string, 0, len(listener.AllowedCIDRs)) + allowedCIDRs = append(allowedCIDRs, listener.AllowedCIDRs...) + return d.Set("allowed_cidrs", allowedCIDRs) + } + + return nil +} + +func setInsertHeaders(_ context.Context, d *schema.ResourceData, listener *edgecloud.Listener) error { + if len(listener.InsertHeaders) > 0 { + return d.Set("insert_headers", map[string]interface{}{ + "X-Forwarded-For": "true", + "X-Forwarded-Port": "true", + "X-Forwarded-Proto": "true", + }) + } + + return nil +} diff --git a/edgecenter/loadbalancer/loadbalancer.go b/edgecenter/loadbalancer/loadbalancer.go index f70e1aea..d7bdc42d 100644 --- a/edgecenter/loadbalancer/loadbalancer.go +++ b/edgecenter/loadbalancer/loadbalancer.go @@ -110,8 +110,3 @@ func loadbalancerSchema() map[string]*schema.Schema { }, } } - -/* - "listeners": [], - "floating_ips": [], -*/ diff --git a/edgecenter/loadbalancer/resource_loadbalancer.go b/edgecenter/loadbalancer/resource_loadbalancer.go index 43c87a1a..92079533 100644 --- a/edgecenter/loadbalancer/resource_loadbalancer.go +++ b/edgecenter/loadbalancer/resource_loadbalancer.go @@ -65,7 +65,7 @@ func resourceEdgeCenterLoadbalancerCreate(ctx context.Context, d *schema.Resourc return diag.Errorf("error creating loadbalancer: %s", err) } - d.SetId(taskResult.LoadBalancers[0]) + d.SetId(taskResult.Loadbalancers[0]) log.Printf("[INFO] Loadbalancer: %s", d.Id()) diff --git a/edgecenter/provider.go b/edgecenter/provider.go index 4f0ee307..8e371b1e 100644 --- a/edgecenter/provider.go +++ b/edgecenter/provider.go @@ -9,6 +9,7 @@ import ( "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/floatingip" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/instance" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lblistener" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/loadbalancer" "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/volume" ) @@ -34,12 +35,14 @@ func Provider() *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ "edgecenter_floatingip": floatingip.DataSourceEdgeCenterFloatingIP(), "edgecenter_instance": instance.DataSourceEdgeCenterInstance(), + "edgecenter_lblistener": lblistener.DataSourceEdgeCenterLbListener(), "edgecenter_loadbalancer": loadbalancer.DataSourceEdgeCenterLoadbalancer(), "edgecenter_volume": volume.DataSourceEdgeCenterVolume(), }, ResourcesMap: map[string]*schema.Resource{ "edgecenter_floatingip": floatingip.ResourceEdgeCenterFloatingIP(), "edgecenter_instance": instance.ResourceEdgeCenterInstance(), + "edgecenter_lblistener": lblistener.ResourceEdgeCenterLbListener(), "edgecenter_loadbalancer": loadbalancer.ResourceEdgeCenterLoadbalancer(), "edgecenter_volume": volume.ResourceEdgeCenterVolume(), }, diff --git a/go.mod b/go.mod index 408af76a..4e92baef 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Edge-Center/terraform-provider-edgecenter go 1.21 require ( - github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231211065040-30ce69f0666f + github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 ) diff --git a/go.sum b/go.sum index 5dcf01a2..2d02bd72 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,12 @@ github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231211064511-3d1d7887164b h github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231211064511-3d1d7887164b/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231211065040-30ce69f0666f h1:XI9oMga3VVBMgMhSuIKYyjcqWAKKqEp/ZhI/acnfHYI= github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231211065040-30ce69f0666f/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212072240-c64911ebe247 h1:pxqkW3MVh4DbhFAYP2CVxQTW59SJrLY5/Hmqc2gCZwU= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212072240-c64911ebe247/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212073651-b3c52a55f6ba h1:AX06f1hgw9Xaw0dLhf5MrLhO6+WrU2ejOhooy/sYIQs= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212073651-b3c52a55f6ba/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8 h1:Eq22tmuTqpXHoUHH1Jow54uy2C785RlWTY7WZcTisnA= +github.com/Edge-Center/edgecentercloud-go v1.0.1-0.20231212080222-fcada367a8d8/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=