diff --git a/.gitignore b/.gitignore index 428aab1..b49988a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ website/node_modules *.test *.iml vendor +.vscode website/vendor diff --git a/serverscom/provider.go b/serverscom/provider.go index a9fa2dc..fd67509 100644 --- a/serverscom/provider.go +++ b/serverscom/provider.go @@ -42,5 +42,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { client.SetupUserAgent("terraform-provider-serverscom") cache = NewCache(client) + serverCollector = NewServerCollector(client) + serverCollector.Run() + return client, nil } diff --git a/serverscom/resource_serverscom_dedicated_server.go b/serverscom/resource_serverscom_dedicated_server.go index 39161d3..ac75780 100644 --- a/serverscom/resource_serverscom_dedicated_server.go +++ b/serverscom/resource_serverscom_dedicated_server.go @@ -26,7 +26,7 @@ func resourceServerscomDedicatedServer() *schema.Resource { Delete: resourceServerscomDedicatedServerDelete, Create: resourceServerscomDedicatedServerCreate, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Timeouts: &schema.ResourceTimeout{ @@ -300,22 +300,23 @@ func resourceServerscomDedicatedServerDelete(d *schema.ResourceData, meta interf } func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*scgo.Client) - - var location *scgo.Location - var serverModel *scgo.ServerModelOption - var operatingSystem *scgo.OperatingSystemOption - var publicUplink *scgo.UplinkOption - var bandwidth *scgo.BandwidthOption - var privateUplink *scgo.UplinkOption - var slots []scgo.DedicatedServerSlotInput - var layouts []scgo.DedicatedServerLayoutInput - var dedicatedServers []scgo.DedicatedServer - - var publicIpv4NetworkId *string - var privateIpv4NetworkId *string - - var err error + var ( + location *scgo.Location + serverModel *scgo.ServerModelOption + operatingSystem *scgo.OperatingSystemOption + publicUplink *scgo.UplinkOption + bandwidth *scgo.BandwidthOption + privateUplink *scgo.UplinkOption + slots []scgo.DedicatedServerSlotInput + layouts []scgo.DedicatedServerLayoutInput + dedicatedServers []scgo.DedicatedServer + dedicatedServer scgo.DedicatedServer + + publicIpv4NetworkId *string + privateIpv4NetworkId *string + + err error + ) input := scgo.DedicatedServerCreateInput{} @@ -329,9 +330,10 @@ func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interf privateIpv4NetworkId = &privateIpv4NetworkIdValue } + hostname := d.Get("hostname").(string) input.Hosts = []scgo.DedicatedServerHostInput{ { - Hostname: d.Get("hostname").(string), + Hostname: hostname, PublicIPv4NetworkID: publicIpv4NetworkId, PrivateIPv4NetworkID: privateIpv4NetworkId, }, @@ -431,16 +433,32 @@ func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interf ctx := context.TODO() - dedicatedServers, err = client.Hosts.CreateDedicatedServers(ctx, input) + resultChan, err := serverCollector.AddRequest(ctx, serverModel.Name, &input) if err != nil { return err } + // waiting for result from collector + result := <-resultChan + if result.Error != nil { + return result.Error + } + + dedicatedServers = result.Servers + if len(dedicatedServers) == 0 { return fmt.Errorf("Invalid dedicated servers count returned by api") } - dedicatedServer := dedicatedServers[0] + // find corresponding server by title matching hostname + for _, server := range dedicatedServers { + if server.Title == hostname { + dedicatedServer = server + } + } + if dedicatedServer.ID == "" { + return fmt.Errorf("Can't find the server with title '%s' in api response", hostname) + } d.SetId(dedicatedServer.ID) diff --git a/serverscom/servers_collector.go b/serverscom/servers_collector.go new file mode 100644 index 0000000..f14ea04 --- /dev/null +++ b/serverscom/servers_collector.go @@ -0,0 +1,150 @@ +package serverscom + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sync" + "time" + + scgo "github.com/serverscom/serverscom-go-client/pkg" +) + +var ( + serverCollector *ServerCollector + + // when timer expires the collector triggers the ExecuteRequests method + serverCollectorTimer = 5 * time.Second +) + +// ServerCollector represents server collector abstraction. +// It accept requests from create events for 'serverscom_dedicated_server' resources +// groups it by checksum based on all server fields except 'hosts' and create these servers in one batch request +type ServerCollector struct { + Client *scgo.Client + Requests map[string][]*Request + Mutex sync.Mutex + Timer *time.Timer +} + +// Request represents a request with create server input and result channel +type Request struct { + Input *scgo.DedicatedServerCreateInput + ResultChan chan Result +} + +// Result represents the api response and error. +// It's used for sending back the result to the create event through the ResultChan +type Result struct { + Servers []scgo.DedicatedServer + Error error +} + +// NewServerCollector creates new ServerCollector +func NewServerCollector(client *scgo.Client) *ServerCollector { + return &ServerCollector{ + Client: client, + Requests: make(map[string][]*Request), + Timer: time.NewTimer(5 * time.Second), + } +} + +// AddRequest adds request to server collector +// Each request resets the serverCollectorTimer +func (sc *ServerCollector) AddRequest(ctx context.Context, model string, request *scgo.DedicatedServerCreateInput) (<-chan Result, error) { + sc.Mutex.Lock() + defer sc.Mutex.Unlock() + + resultChan := make(chan Result, 1) + checksum, err := calculateServerChecksum(*request) + if err != nil { + return nil, err + } + sc.Requests[checksum] = append(sc.Requests[checksum], &Request{Input: request, ResultChan: resultChan}) + + if !sc.Timer.Stop() { + select { + case <-sc.Timer.C: + default: + } + } + sc.Timer.Reset(serverCollectorTimer) + + return resultChan, nil +} + +// ExecuteRequests triggers when timer expires and runs CreateServersBatch for each requests checksum group +func (sc *ServerCollector) ExecuteRequests() { + sc.Mutex.Lock() + defer sc.Mutex.Unlock() + + for checksum, requests := range sc.Requests { + CreateServersBatch(sc.Client, requests) + sc.Requests[checksum] = nil + } +} + +// Run runs the collector to listen for requests +func (sc *ServerCollector) Run() { + go func() { + for { + <-sc.Timer.C + sc.ExecuteRequests() + } + }() +} + +// CreateServersBatch aggregates hostnames from all requests in one input and creates these servers in one api request +func CreateServersBatch(client *scgo.Client, requests []*Request) { + if len(requests) == 0 { + return + } + for _, req := range requests { + defer close(req.ResultChan) + } + + // combine hosts input + // for any duplicate hostname return error + uniqueHostnames := make(map[string]bool) + createInput := *requests[0].Input + createInput.Hosts = nil + for _, req := range requests { + for _, host := range req.Input.Hosts { + if _, ok := uniqueHostnames[host.Hostname]; ok { + result := Result{ + Error: fmt.Errorf("duplicate hostname found: %s", host.Hostname), + } + req.ResultChan <- result + continue + } + uniqueHostnames[host.Hostname] = true + createInput.Hosts = append(createInput.Hosts, host) + } + } + + // create servers + dedicatedServers, err := client.Hosts.CreateDedicatedServers(context.TODO(), createInput) + result := Result{ + Servers: dedicatedServers, + Error: err, + } + + for _, req := range requests { + req.ResultChan <- result + } +} + +// calculateServerChecksum generate checksum for server create input excepting the Hosts field +func calculateServerChecksum(input scgo.DedicatedServerCreateInput) (string, error) { + input.Hosts = []scgo.DedicatedServerHostInput{} + + data, err := json.Marshal(input) + if err != nil { + return "", err + } + + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]), nil +}