2016-04-14 20:29:27 -04:00
|
|
|
package triton
|
|
|
|
|
|
|
|
import (
|
2017-05-05 11:20:21 -04:00
|
|
|
"context"
|
2016-04-14 20:29:27 -04:00
|
|
|
"errors"
|
2017-05-10 12:12:24 -04:00
|
|
|
"net/http"
|
builder/triton: Add a data source for source_machine_image
fixes: #5476
Based on this new template addition:
```
{
"variables": {
"image_version": "",
"triton_account": "",
"triton_key_id": "",
"triton_key_material": ""
},
"builders": [{
"type": "triton",
"triton_account": "{{user `triton_account`}}",
"triton_key_id": "{{user `triton_key_id`}}",
"triton_key_material": "{{user `triton_key_material`}}",
"source_machine_package": "g4-highcpu-128M",
"source_machine_image_filter": {
"name": "ubuntu-16.04",
"most_recent": "true"
},
"ssh_username": "root",
"image_version": "{{user `image_version`}}",
"image_name": "teamcity-server"
}],
"provisioners": [
{
"type": "shell",
"start_retry_timeout": "10m",
"inline": [
"sudo apt-get update -y",
"sudo apt-get install -y nginx"
]
}
]
}
```
I got the following output from packer:
```
packer-testing % make image
packer build \
-var "triton_account=stack72_joyent" \
-var "triton_key_id=40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b" \
-var "triton_key_material=""" \
-var "image_version=1.0.0" \
new-template.json
triton output will be in this color.
==> triton: Selecting an image based on search criteria
==> triton: Based, on given search criteria, Machine ID is: "7b5981c4-1889-11e7-b4c5-3f3bdfc9b88b"
==> triton: Waiting for source machine to become available...
==> triton: Waiting for SSH to become available...
==> triton: Connected to SSH!
==> triton: Provisioning with shell script: /var/folders/_p/2_zj9lqn4n11fx20qy787p7c0000gn/T/packer-shell797317310
triton: Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
triton: Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease
```
I can verify from the triton cli tools that the id `7b5981c4` (from the packer output) is indeed the correct ID
```
terraform [master●] % triton images name=~ubuntu-16.04
SHORTID NAME VERSION FLAGS OS TYPE PUBDATE
49b22aec ubuntu-16.04 20160427 P linux lx-dataset 2016-04-27
675834a0 ubuntu-16.04 20160505 P linux lx-dataset 2016-05-05
4edaa46a ubuntu-16.04 20160516 P linux lx-dataset 2016-05-16
05140a7e ubuntu-16.04 20160601 P linux lx-dataset 2016-06-01
e331b22a ubuntu-16.04 20161004 P linux lx-dataset 2016-10-04
8879c758 ubuntu-16.04 20161213 P linux lx-dataset 2016-12-13
7b5981c4 ubuntu-16.04 20170403 P linux lx-dataset 2017-04-03 <------- THIS IS THE LATEST UBUNTU IMAGE
```
2017-10-30 13:26:42 -04:00
|
|
|
"sort"
|
2017-11-08 09:33:15 -05:00
|
|
|
"time"
|
builder/triton: Add a data source for source_machine_image
fixes: #5476
Based on this new template addition:
```
{
"variables": {
"image_version": "",
"triton_account": "",
"triton_key_id": "",
"triton_key_material": ""
},
"builders": [{
"type": "triton",
"triton_account": "{{user `triton_account`}}",
"triton_key_id": "{{user `triton_key_id`}}",
"triton_key_material": "{{user `triton_key_material`}}",
"source_machine_package": "g4-highcpu-128M",
"source_machine_image_filter": {
"name": "ubuntu-16.04",
"most_recent": "true"
},
"ssh_username": "root",
"image_version": "{{user `image_version`}}",
"image_name": "teamcity-server"
}],
"provisioners": [
{
"type": "shell",
"start_retry_timeout": "10m",
"inline": [
"sudo apt-get update -y",
"sudo apt-get install -y nginx"
]
}
]
}
```
I got the following output from packer:
```
packer-testing % make image
packer build \
-var "triton_account=stack72_joyent" \
-var "triton_key_id=40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b" \
-var "triton_key_material=""" \
-var "image_version=1.0.0" \
new-template.json
triton output will be in this color.
==> triton: Selecting an image based on search criteria
==> triton: Based, on given search criteria, Machine ID is: "7b5981c4-1889-11e7-b4c5-3f3bdfc9b88b"
==> triton: Waiting for source machine to become available...
==> triton: Waiting for SSH to become available...
==> triton: Connected to SSH!
==> triton: Provisioning with shell script: /var/folders/_p/2_zj9lqn4n11fx20qy787p7c0000gn/T/packer-shell797317310
triton: Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
triton: Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease
```
I can verify from the triton cli tools that the id `7b5981c4` (from the packer output) is indeed the correct ID
```
terraform [master●] % triton images name=~ubuntu-16.04
SHORTID NAME VERSION FLAGS OS TYPE PUBDATE
49b22aec ubuntu-16.04 20160427 P linux lx-dataset 2016-04-27
675834a0 ubuntu-16.04 20160505 P linux lx-dataset 2016-05-05
4edaa46a ubuntu-16.04 20160516 P linux lx-dataset 2016-05-16
05140a7e ubuntu-16.04 20160601 P linux lx-dataset 2016-06-01
e331b22a ubuntu-16.04 20161004 P linux lx-dataset 2016-10-04
8879c758 ubuntu-16.04 20161213 P linux lx-dataset 2016-12-13
7b5981c4 ubuntu-16.04 20170403 P linux lx-dataset 2017-04-03 <------- THIS IS THE LATEST UBUNTU IMAGE
```
2017-10-30 13:26:42 -04:00
|
|
|
|
2017-04-04 16:39:01 -04:00
|
|
|
"github.com/hashicorp/packer/packer"
|
2017-10-31 11:02:15 -04:00
|
|
|
"github.com/joyent/triton-go/compute"
|
2018-01-16 12:48:25 -05:00
|
|
|
terrors "github.com/joyent/triton-go/errors"
|
2016-04-14 20:29:27 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
type driverTriton struct {
|
2017-10-31 11:02:15 -04:00
|
|
|
client *Client
|
2016-04-14 20:29:27 -04:00
|
|
|
ui packer.Ui
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewDriverTriton(ui packer.Ui, config Config) (Driver, error) {
|
|
|
|
client, err := config.AccessConfig.CreateTritonClient()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &driverTriton{
|
|
|
|
client: client,
|
|
|
|
ui: ui,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
builder/triton: Add a data source for source_machine_image
fixes: #5476
Based on this new template addition:
```
{
"variables": {
"image_version": "",
"triton_account": "",
"triton_key_id": "",
"triton_key_material": ""
},
"builders": [{
"type": "triton",
"triton_account": "{{user `triton_account`}}",
"triton_key_id": "{{user `triton_key_id`}}",
"triton_key_material": "{{user `triton_key_material`}}",
"source_machine_package": "g4-highcpu-128M",
"source_machine_image_filter": {
"name": "ubuntu-16.04",
"most_recent": "true"
},
"ssh_username": "root",
"image_version": "{{user `image_version`}}",
"image_name": "teamcity-server"
}],
"provisioners": [
{
"type": "shell",
"start_retry_timeout": "10m",
"inline": [
"sudo apt-get update -y",
"sudo apt-get install -y nginx"
]
}
]
}
```
I got the following output from packer:
```
packer-testing % make image
packer build \
-var "triton_account=stack72_joyent" \
-var "triton_key_id=40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b" \
-var "triton_key_material=""" \
-var "image_version=1.0.0" \
new-template.json
triton output will be in this color.
==> triton: Selecting an image based on search criteria
==> triton: Based, on given search criteria, Machine ID is: "7b5981c4-1889-11e7-b4c5-3f3bdfc9b88b"
==> triton: Waiting for source machine to become available...
==> triton: Waiting for SSH to become available...
==> triton: Connected to SSH!
==> triton: Provisioning with shell script: /var/folders/_p/2_zj9lqn4n11fx20qy787p7c0000gn/T/packer-shell797317310
triton: Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
triton: Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease
```
I can verify from the triton cli tools that the id `7b5981c4` (from the packer output) is indeed the correct ID
```
terraform [master●] % triton images name=~ubuntu-16.04
SHORTID NAME VERSION FLAGS OS TYPE PUBDATE
49b22aec ubuntu-16.04 20160427 P linux lx-dataset 2016-04-27
675834a0 ubuntu-16.04 20160505 P linux lx-dataset 2016-05-05
4edaa46a ubuntu-16.04 20160516 P linux lx-dataset 2016-05-16
05140a7e ubuntu-16.04 20160601 P linux lx-dataset 2016-06-01
e331b22a ubuntu-16.04 20161004 P linux lx-dataset 2016-10-04
8879c758 ubuntu-16.04 20161213 P linux lx-dataset 2016-12-13
7b5981c4 ubuntu-16.04 20170403 P linux lx-dataset 2017-04-03 <------- THIS IS THE LATEST UBUNTU IMAGE
```
2017-10-30 13:26:42 -04:00
|
|
|
func (d *driverTriton) GetImage(config Config) (string, error) {
|
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
images, err := computeClient.Images().List(context.Background(), &compute.ListImagesInput{
|
|
|
|
Name: config.MachineImageFilters.Name,
|
|
|
|
OS: config.MachineImageFilters.OS,
|
|
|
|
Version: config.MachineImageFilters.Version,
|
|
|
|
Public: config.MachineImageFilters.Public,
|
|
|
|
Type: config.MachineImageFilters.Type,
|
|
|
|
State: config.MachineImageFilters.State,
|
|
|
|
Owner: config.MachineImageFilters.Owner,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(images) == 0 {
|
|
|
|
return "", errors.New("No images found in your search. Please refine your search criteria")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(images) > 1 {
|
|
|
|
if !config.MachineImageFilters.MostRecent {
|
|
|
|
return "", errors.New("More than 1 machine image was found in your search. Please refine your search criteria")
|
|
|
|
} else {
|
|
|
|
return mostRecentImages(images).ID, nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return images[0].ID, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-14 20:29:27 -04:00
|
|
|
func (d *driverTriton) CreateImageFromMachine(machineId string, config Config) (string, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
image, err := computeClient.Images().CreateFromMachine(context.Background(), &compute.CreateImageFromMachineInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
MachineID: machineId,
|
2016-04-14 20:29:27 -04:00
|
|
|
Name: config.ImageName,
|
|
|
|
Version: config.ImageVersion,
|
|
|
|
Description: config.ImageDescription,
|
2017-04-26 15:07:45 -04:00
|
|
|
HomePage: config.ImageHomepage,
|
2016-04-14 20:29:27 -04:00
|
|
|
EULA: config.ImageEULA,
|
|
|
|
ACL: config.ImageACL,
|
|
|
|
Tags: config.ImageTags,
|
2017-04-26 15:07:45 -04:00
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2017-04-26 15:07:45 -04:00
|
|
|
return image.ID, err
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *driverTriton) CreateMachine(config Config) (string, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
input := &compute.CreateInstanceInput{
|
2016-04-14 20:29:27 -04:00
|
|
|
Package: config.MachinePackage,
|
|
|
|
Image: config.MachineImage,
|
|
|
|
Metadata: config.MachineMetadata,
|
|
|
|
Tags: config.MachineTags,
|
|
|
|
FirewallEnabled: config.MachineFirewallEnabled,
|
|
|
|
}
|
|
|
|
|
2016-12-28 10:01:08 -05:00
|
|
|
if config.MachineName == "" {
|
|
|
|
// If not supplied generate a name for the source VM: "packer-builder-[image_name]".
|
|
|
|
// The version is not used because it can contain characters invalid for a VM name.
|
2017-04-26 15:07:45 -04:00
|
|
|
input.Name = "packer-builder-" + config.ImageName
|
2016-12-28 10:01:08 -05:00
|
|
|
} else {
|
2017-04-26 15:07:45 -04:00
|
|
|
input.Name = config.MachineName
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(config.MachineNetworks) > 0 {
|
|
|
|
input.Networks = config.MachineNetworks
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
2017-10-31 11:02:15 -04:00
|
|
|
machine, err := computeClient.Instances().Create(context.Background(), input)
|
2016-04-14 20:29:27 -04:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2017-04-26 15:07:45 -04:00
|
|
|
return machine.ID, nil
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *driverTriton) DeleteImage(imageId string) error {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
return computeClient.Images().Delete(context.Background(), &compute.DeleteImageInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ImageID: imageId,
|
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (d *driverTriton) DeleteMachine(machineId string) error {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
return computeClient.Instances().Delete(context.Background(), &compute.DeleteInstanceInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ID: machineId,
|
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
2017-04-26 15:07:45 -04:00
|
|
|
func (d *driverTriton) GetMachineIP(machineId string) (string, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
machine, err := computeClient.Instances().Get(context.Background(), &compute.GetInstanceInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ID: machineId,
|
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return machine.PrimaryIP, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *driverTriton) StopMachine(machineId string) error {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
return computeClient.Instances().Stop(context.Background(), &compute.StopInstanceInput{
|
|
|
|
InstanceID: machineId,
|
2017-04-26 15:07:45 -04:00
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// waitForMachineState uses the supplied client to wait for the state of
|
|
|
|
// the machine with the given ID to reach the state described in state.
|
|
|
|
// If timeout is reached before the machine reaches the required state, an
|
|
|
|
// error is returned. If the machine reaches the target state within the
|
|
|
|
// timeout, nil is returned.
|
|
|
|
func (d *driverTriton) WaitForMachineState(machineId string, state string, timeout time.Duration) error {
|
|
|
|
return waitFor(
|
|
|
|
func() (bool, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
machine, err := computeClient.Instances().Get(context.Background(), &compute.GetInstanceInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ID: machineId,
|
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
if machine == nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return machine.State == state, err
|
|
|
|
},
|
|
|
|
3*time.Second,
|
|
|
|
timeout,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// waitForMachineDeletion uses the supplied client to wait for the machine
|
|
|
|
// with the given ID to be deleted. It is expected that the API call to delete
|
|
|
|
// the machine has already been issued at this point.
|
|
|
|
func (d *driverTriton) WaitForMachineDeletion(machineId string, timeout time.Duration) error {
|
|
|
|
return waitFor(
|
|
|
|
func() (bool, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
_, err := computeClient.Instances().Get(context.Background(), &compute.GetInstanceInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ID: machineId,
|
|
|
|
})
|
2017-05-10 12:12:24 -04:00
|
|
|
if err != nil {
|
|
|
|
// Return true only when we receive a 410 (Gone) response. A 404
|
|
|
|
// indicates that the machine is being deleted whereas a 410 indicates
|
|
|
|
// that this process has completed.
|
2018-01-16 12:48:25 -05:00
|
|
|
if terrors.IsSpecificStatusCode(err, http.StatusGone) {
|
2017-05-10 12:12:24 -04:00
|
|
|
return true, nil
|
|
|
|
}
|
2016-12-28 10:01:08 -05:00
|
|
|
}
|
|
|
|
|
2016-04-14 20:29:27 -04:00
|
|
|
return false, err
|
|
|
|
},
|
|
|
|
3*time.Second,
|
|
|
|
timeout,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *driverTriton) WaitForImageCreation(imageId string, timeout time.Duration) error {
|
|
|
|
return waitFor(
|
|
|
|
func() (bool, error) {
|
2017-10-31 11:02:15 -04:00
|
|
|
computeClient, _ := d.client.Compute()
|
|
|
|
image, err := computeClient.Images().Get(context.Background(), &compute.GetImageInput{
|
2017-04-26 15:07:45 -04:00
|
|
|
ImageID: imageId,
|
|
|
|
})
|
2016-04-14 20:29:27 -04:00
|
|
|
if image == nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2017-11-08 09:33:15 -05:00
|
|
|
return image.State == "active", err
|
2016-04-14 20:29:27 -04:00
|
|
|
},
|
|
|
|
3*time.Second,
|
|
|
|
timeout,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func waitFor(f func() (bool, error), every, timeout time.Duration) error {
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
for time.Since(start) <= timeout {
|
|
|
|
stop, err := f()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if stop {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(every)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors.New("Timed out while waiting for resource change")
|
|
|
|
}
|
builder/triton: Add a data source for source_machine_image
fixes: #5476
Based on this new template addition:
```
{
"variables": {
"image_version": "",
"triton_account": "",
"triton_key_id": "",
"triton_key_material": ""
},
"builders": [{
"type": "triton",
"triton_account": "{{user `triton_account`}}",
"triton_key_id": "{{user `triton_key_id`}}",
"triton_key_material": "{{user `triton_key_material`}}",
"source_machine_package": "g4-highcpu-128M",
"source_machine_image_filter": {
"name": "ubuntu-16.04",
"most_recent": "true"
},
"ssh_username": "root",
"image_version": "{{user `image_version`}}",
"image_name": "teamcity-server"
}],
"provisioners": [
{
"type": "shell",
"start_retry_timeout": "10m",
"inline": [
"sudo apt-get update -y",
"sudo apt-get install -y nginx"
]
}
]
}
```
I got the following output from packer:
```
packer-testing % make image
packer build \
-var "triton_account=stack72_joyent" \
-var "triton_key_id=40:9d:d3:f9:0b:86:62:48:f4:2e:a5:8e:43:00:2a:9b" \
-var "triton_key_material=""" \
-var "image_version=1.0.0" \
new-template.json
triton output will be in this color.
==> triton: Selecting an image based on search criteria
==> triton: Based, on given search criteria, Machine ID is: "7b5981c4-1889-11e7-b4c5-3f3bdfc9b88b"
==> triton: Waiting for source machine to become available...
==> triton: Waiting for SSH to become available...
==> triton: Connected to SSH!
==> triton: Provisioning with shell script: /var/folders/_p/2_zj9lqn4n11fx20qy787p7c0000gn/T/packer-shell797317310
triton: Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
triton: Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease
```
I can verify from the triton cli tools that the id `7b5981c4` (from the packer output) is indeed the correct ID
```
terraform [master●] % triton images name=~ubuntu-16.04
SHORTID NAME VERSION FLAGS OS TYPE PUBDATE
49b22aec ubuntu-16.04 20160427 P linux lx-dataset 2016-04-27
675834a0 ubuntu-16.04 20160505 P linux lx-dataset 2016-05-05
4edaa46a ubuntu-16.04 20160516 P linux lx-dataset 2016-05-16
05140a7e ubuntu-16.04 20160601 P linux lx-dataset 2016-06-01
e331b22a ubuntu-16.04 20161004 P linux lx-dataset 2016-10-04
8879c758 ubuntu-16.04 20161213 P linux lx-dataset 2016-12-13
7b5981c4 ubuntu-16.04 20170403 P linux lx-dataset 2017-04-03 <------- THIS IS THE LATEST UBUNTU IMAGE
```
2017-10-30 13:26:42 -04:00
|
|
|
|
|
|
|
func mostRecentImages(images []*compute.Image) *compute.Image {
|
|
|
|
return sortImages(images)[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
type imageSort []*compute.Image
|
|
|
|
|
|
|
|
func sortImages(images []*compute.Image) []*compute.Image {
|
|
|
|
sortedImages := images
|
|
|
|
sort.Sort(sort.Reverse(imageSort(sortedImages)))
|
|
|
|
return sortedImages
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a imageSort) Len() int {
|
|
|
|
return len(a)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a imageSort) Swap(i, j int) {
|
|
|
|
a[i], a[j] = a[j], a[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a imageSort) Less(i, j int) bool {
|
|
|
|
itime := a[i].PublishedAt
|
|
|
|
jtime := a[j].PublishedAt
|
|
|
|
return itime.Unix() < jtime.Unix()
|
|
|
|
}
|