Merge pull request #3377 from Sh4d1/update_scaleway_provider

Update Scaleway Provider with new Go SDK
This commit is contained in:
Rolf Neugebauer 2019-07-05 18:33:14 +01:00 committed by GitHub
commit 66cd2b6ee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 8700 additions and 4656 deletions

View File

@ -78,6 +78,7 @@ Currently supported platforms are:
- [Google Cloud](docs/platform-gcp.md) `[x86_64]`
- [Microsoft Azure](docs/platform-azure.md) `[x86_64]`
- [OpenStack](docs/platform-openstack.md) `[x86_64]`
- [Scaleway](docs/platform-scaleway.md) `[x86_64]`
- Baremetal:
- [packet.net](docs/platform-packet.md) `[x86_64, arm64]`
- [Raspberry Pi Model 3b](docs/platform-rpi3.md) `[arm64]`

View File

@ -3,14 +3,11 @@
This is a quick guide to run LinuxKit on Scaleway (only VPS x86_64 for now)
## Setup
Before you proceed it's recommanded that you set up the [Scaleway CLI](https://github.com/scaleway/scaleway-cli/)
and perform an `scw login`. This will create a `$HOME/.scwrc` file containing the required API token.
You can also use the `SCW_TOKEN` environment variable to set a Scaleway token.
The `-token` flag of the `linuxkit push scaleway` and `linuxkit run scaleway` can also be used.
The environment variable `SCW_TARGET_REGION` is used to set the region (there is also the `-region` flag)
You must create a Scaleway Secret Key (available ine the [Scaleway Console](https://console.scaleway.com/account/credentials)) first.
Then you can use it either with the `SCW_SECRET_KEY` environment variable or with the `-secret-key` flag of the `linuxkit push scaleway` and `linuxkit run scaleway` commands.
The environment variable `SCW_DEFAULT_ZONE` is used to set the zone (there is also the `-zone` flag)
## Build an image
@ -33,13 +30,13 @@ It can be overidden with the `-img-name` flag or the `SCW_IMAGE_NAME` environmen
**Note 1:** If an image (and snapshot) of the same name exists, it will be replaced.
**Note 2:** The image is region specific: if you create an image in `par1` you can't use is in `ams1`.
**Note 2:** The image is zone specific: if you create an image in `par1` you can't use is in `ams1`.
### Push process
Building a Scaleway image have a special process. Basically:
* Create an `image-builder` instance with an additional volume, based on Ubuntu Xenial (only x86_64 for now)
* Create an `image-builder` instance with an additional volume, based on Ubuntu Bionic (only x86_64 for now)
* Copy the ISO image on this instance
* Use `dd` to write the image on the additional volume (`/dev/vdb` by default)
* Terminate the instance, create a snapshot, and create an image from the snapshot

View File

@ -20,11 +20,12 @@ func pushScaleway(args []string) {
flags.PrintDefaults()
}
nameFlag := flags.String("img-name", "", "Overrides the name used to identify the image name in Scaleway's images. Defaults to the base of 'path' with the '.iso' suffix removed")
tokenFlag := flags.String("token", "", "Token to connet to Scaleway API")
secretKeyFlag := flags.String("secret-key", "", "Secret Key to connet to Scaleway API")
sshKeyFlag := flags.String("ssh-key", os.Getenv("HOME")+"/.ssh/id_rsa", "SSH key file")
instanceIDFlag := flags.String("instance-id", "", "Instance ID of a running Scaleway instance, with a second volume.")
deviceNameFlag := flags.String("device-name", "/dev/vdb", "Device name on which the image will be copied")
regionFlag := flags.String("region", defaultScalewayRegion, "Select scaleway region")
zoneFlag := flags.String("zone", defaultScalewayZone, "Select Scaleway zone")
projectIDFlag := flags.String("project-id", "", "Select Scaleway's project ID")
noCleanFlag := flags.Bool("no-clean", false, "Do not remove temporary instance and volumes")
if err := flags.Parse(args); err != nil {
@ -40,11 +41,12 @@ func pushScaleway(args []string) {
path := remArgs[0]
name := getStringValue(scalewayNameVar, *nameFlag, "")
token := getStringValue(tokenVar, *tokenFlag, "")
secretKey := getStringValue(secretKeyVar, *secretKeyFlag, "")
sshKeyFile := getStringValue(sshKeyVar, *sshKeyFlag, "")
instanceID := getStringValue(instanceIDVar, *instanceIDFlag, "")
deviceName := getStringValue(deviceNameVar, *deviceNameFlag, "")
region := getStringValue(regionVar, *regionFlag, defaultScalewayRegion)
zone := getStringValue(zoneVar, *zoneFlag, defaultScalewayZone)
projectID := getStringValue(projectIDVar, *projectIDFlag, "")
const suffix = ".iso"
if name == "" {
@ -52,7 +54,7 @@ func pushScaleway(args []string) {
name = filepath.Base(name)
}
client, err := NewScalewayClient(token, region)
client, err := NewScalewayClient(secretKey, zone, projectID)
if err != nil {
log.Fatalf("Unable to connect to Scaleway: %v", err)
}

View File

@ -10,15 +10,16 @@ import (
)
const (
defaultScalewayInstanceType = "VC1S"
defaultScalewayRegion = "par1"
defaultScalewayInstanceType = "DEV1-S"
defaultScalewayZone = "par1"
scalewayNameVar = "SCW_IMAGE_NAME" // non-standard
tokenVar = "SCW_TOKEN" // non-standard
secretKeyVar = "SCW_SECRET_KEY" // non-standard
sshKeyVar = "SCW_SSH_KEY_FILE" // non-standard
instanceIDVar = "SCW_INSTANCE_ID" // non-standard
deviceNameVar = "SCW_DEVICE_NAME" // non-standard
regionVar = "SCW_TARGET_REGION"
scwZoneVar = "SCW_DEFAULT_ZONE"
projectIDVar = "SCW_DEFAULT_PROJECT_ID"
instanceTypeVar = "SCW_RUN_TYPE" // non-standard
)
@ -35,8 +36,9 @@ func runScaleway(args []string) {
}
instanceTypeFlag := flags.String("instance-type", defaultScalewayInstanceType, "Scaleway instance type")
instanceNameFlag := flags.String("instance-name", "linuxkit", "Name of the create instance, default to the image name")
tokenFlag := flags.String("token", "", "Token to connect to Scaleway API")
regionFlag := flags.String("region", defaultScalewayRegion, "Select Scaleway region")
secretKeyFlag := flags.String("secret-key", "", "Secret Key to connect to Scaleway API")
zoneFlag := flags.String("zone", defaultScalewayZone, "Select Scaleway zone")
projectIDFlag := flags.String("project-id", "", "Select Scaleway's project ID")
cleanFlag := flags.Bool("clean", false, "Remove instance")
noAttachFlag := flags.Bool("no-attach", false, "Don't attach to serial port, you will have to connect to instance manually")
@ -54,10 +56,11 @@ func runScaleway(args []string) {
instanceType := getStringValue(instanceTypeVar, *instanceTypeFlag, defaultScalewayInstanceType)
instanceName := getStringValue("", *instanceNameFlag, name)
token := getStringValue(tokenVar, *tokenFlag, "")
region := getStringValue(regionVar, *regionFlag, defaultScalewayRegion)
secretKey := getStringValue(secretKeyVar, *secretKeyFlag, "")
zone := getStringValue(scwZoneVar, *zoneFlag, defaultScalewayZone)
projectID := getStringValue(projectIDVar, *projectIDFlag, "")
client, err := NewScalewayClient(token, region)
client, err := NewScalewayClient(secretKey, zone, projectID)
if err != nil {
log.Fatalf("Unable to connect to Scaleway: %v", err)
}

View File

@ -2,7 +2,6 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
@ -11,143 +10,189 @@ import (
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/ScaleFT/sshkeys"
gotty "github.com/moul/gotty-client"
scw "github.com/scaleway/go-scaleway"
"github.com/scaleway/go-scaleway/logger"
"github.com/scaleway/go-scaleway/types"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/scwconfig"
"github.com/scaleway/scaleway-sdk-go/utils"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
var (
defaultScalewayCommercialType = "DEV1-S"
defaultScalewayImageName = "Ubuntu Bionic"
defaultScalewayImageArch = "x86_64"
defaultVolumeSize = uint64(10000000000)
)
// ScalewayClient contains state required for communication with Scaleway as well as the instance
type ScalewayClient struct {
api *scw.ScalewayAPI
fileName string
region string
sshConfig *ssh.ClientConfig
}
// ScalewayConfig contains required field to read scaleway config file
type ScalewayConfig struct {
Organization string `json:"organization"`
Token string `json:"token"`
Version string `json:"version"`
instanceAPI *instance.API
marketplaceAPI *marketplace.API
fileName string
zone string
sshConfig *ssh.ClientConfig
secretKey string
}
// NewScalewayClient creates a new scaleway client
func NewScalewayClient(token, region string) (*ScalewayClient, error) {
func NewScalewayClient(secretKey, zone, projectID string) (*ScalewayClient, error) {
var scwClient *scw.Client
log.Debugf("Connecting to Scaleway")
organization := ""
if token == "" {
log.Debugf("Using .scwrc file to get token")
homeDir := os.Getenv("HOME")
if homeDir == "" {
homeDir = os.Getenv("USERPROFILE") // Windows support
}
if homeDir == "" {
return nil, fmt.Errorf("Home directory not found")
}
swrcPath := filepath.Join(homeDir, ".scwrc")
file, err := ioutil.ReadFile(swrcPath)
if err != nil {
return nil, fmt.Errorf("Error reading Scaleway config file: %v", err)
}
var scalewayConfig ScalewayConfig
err = json.Unmarshal(file, &scalewayConfig)
if err != nil {
return nil, fmt.Errorf("Error during unmarshal of Scaleway config file: %v", err)
}
token = scalewayConfig.Token
organization = scalewayConfig.Organization
}
api, err := scw.NewScalewayAPI(organization, token, "", region)
if err != nil {
return nil, err
}
l := logger.NewDisableLogger()
api.Logger = l
if organization == "" {
organisations, err := api.GetOrganization()
if secretKey == "" {
config, err := scwconfig.Load()
if err != nil {
return nil, err
}
scwClient, err = scw.NewClient(
scw.WithConfig(config),
)
if err != nil {
return nil, err
}
} else {
scwZone, err := utils.ParseZone(zone)
if err != nil {
return nil, err
}
scwClient, err = scw.NewClient(
scw.WithAuth("", secretKey),
scw.WithDefaultZone(scwZone),
scw.WithDefaultProjectID(projectID),
)
if err != nil {
return nil, err
}
api.Organization = organisations.Organizations[0].ID
}
instanceAPI := instance.NewAPI(scwClient)
marketplaceAPI := marketplace.NewAPI(scwClient)
client := &ScalewayClient{
api: api,
fileName: "",
region: region,
instanceAPI: instanceAPI,
marketplaceAPI: marketplaceAPI,
zone: zone,
fileName: "",
secretKey: secretKey,
}
return client, nil
}
func (s *ScalewayClient) getImageID(imageName, commercialType, arch string) (string, error) {
imagesResp, err := s.marketplaceAPI.ListImages(&marketplace.ListImagesRequest{})
if err != nil {
return "", err
}
for _, image := range imagesResp.Images {
if image.Name == imageName {
for _, version := range image.Versions {
for _, localImage := range version.LocalImages {
if localImage.Arch == arch {
for _, compatibleCommercialType := range localImage.CompatibleCommercialTypes {
if compatibleCommercialType == commercialType {
return localImage.ID, nil
}
}
}
}
}
}
}
return "", errors.New("No image matching given requests")
}
// CreateInstance create an instance with one additional volume
func (s *ScalewayClient) CreateInstance() (string, error) {
// get the Ubuntu Xenial image id
image, err := s.api.GetImageID("Ubuntu Xenial", "x86_64") // TODO fix arch and use from args
imageID, err := s.getImageID(defaultScalewayImageName, defaultScalewayCommercialType, defaultScalewayImageArch)
if err != nil {
return "", err
}
imageID := image.Identifier
var serverDefinition types.ScalewayServerDefinition
serverDefinition.Name = "linuxkit-builder"
serverDefinition.Image = &imageID
serverDefinition.CommercialType = "VC1M" // TODO use args?
// creation of second volume
var volumeDefinition types.ScalewayVolumeDefinition
volumeDefinition.Name = "linuxkit-builder-volume"
volumeDefinition.Size = 50000000000 // FIX remove hardcoded value
volumeDefinition.Type = "l_ssd"
createVolumeRequest := &instance.CreateVolumeRequest{
Name: "linuxkit-builder-volume",
VolumeType: "l_ssd",
Size: &defaultVolumeSize,
}
log.Debugf("Creating volume on Scaleway")
volumeID, err := s.api.PostVolume(volumeDefinition)
volumeResp, err := s.instanceAPI.CreateVolume(createVolumeRequest)
if err != nil {
return "", err
}
serverDefinition.Volumes = make(map[string]string)
serverDefinition.Volumes["1"] = volumeID
volumeMap := make(map[string]*instance.VolumeTemplate)
volumeTemplate := &instance.VolumeTemplate{
Size: defaultVolumeSize,
}
volumeMap["0"] = volumeTemplate
serverID, err := s.api.PostServer(serverDefinition)
createServerRequest := &instance.CreateServerRequest{
Name: "linuxkit-builder",
CommercialType: defaultScalewayCommercialType,
DynamicIPRequired: true,
Image: imageID,
EnableIPv6: false,
BootType: instance.ServerBootTypeLocal,
Volumes: volumeMap,
}
serverResp, err := s.instanceAPI.CreateServer(createServerRequest)
if err != nil {
return "", err
}
log.Debugf("Created server %s on Scaleway", serverID)
return serverID, nil
attachVolumeRequest := &instance.AttachVolumeRequest{
ServerID: serverResp.Server.ID,
VolumeID: volumeResp.Volume.ID,
}
_, err = s.instanceAPI.AttachVolume(attachVolumeRequest)
if err != nil {
return "", nil
}
log.Debugf("Created server %s on Scaleway", serverResp.Server.ID)
return serverResp.Server.ID, nil
}
// GetSecondVolumeID returns the ID of the second volume of the server
func (s *ScalewayClient) GetSecondVolumeID(instanceID string) (string, error) {
server, err := s.api.GetServer(instanceID)
getServerRequest := &instance.GetServerRequest{
ServerID: instanceID,
}
serverResp, err := s.instanceAPI.GetServer(getServerRequest)
if err != nil {
return "", err
}
secondVolume, ok := server.Volumes["1"]
secondVolume, ok := serverResp.Server.Volumes["1"]
if !ok {
return "", errors.New("No second volume found")
}
return secondVolume.Identifier, nil
return secondVolume.ID, nil
}
// BootInstanceAndWait boots and wait for instance to be booted
func (s *ScalewayClient) BootInstanceAndWait(instanceID string) error {
err := s.api.PostServerAction(instanceID, "poweron")
serverActionRequest := &instance.ServerActionRequest{
ServerID: instanceID,
Action: instance.ServerActionPoweron,
}
_, err := s.instanceAPI.ServerAction(serverActionRequest)
if err != nil {
return err
}
@ -156,14 +201,17 @@ func (s *ScalewayClient) BootInstanceAndWait(instanceID string) error {
// code taken from scaleway-cli, could need some changes
promise := make(chan bool)
var server *types.ScalewayServer
var currentState string
var server *instance.Server
var currentState instance.ServerState
go func() {
defer close(promise)
for {
server, err = s.api.GetServer(instanceID)
serverResp, err := s.instanceAPI.GetServer(&instance.GetServerRequest{
ServerID: instanceID,
})
server = serverResp.Server
if err != nil {
promise <- false
return
@ -173,20 +221,17 @@ func (s *ScalewayClient) BootInstanceAndWait(instanceID string) error {
currentState = server.State
}
if server.State == "running" {
if server.State == instance.ServerStateRunning {
break
}
if server.State == "stopped" {
if server.State == instance.ServerStateStopped {
promise <- false
return
}
time.Sleep(1 * time.Second)
}
ip := server.PublicAddress.IP
if ip == "" && server.EnableIPV6 {
ip = fmt.Sprintf("[%s]", server.IPV6.Address)
}
ip := server.PublicIP.Address.String()
dest := fmt.Sprintf("%s:22", ip)
for {
conn, err := net.Dial("tcp", dest)
@ -232,7 +277,16 @@ func getSSHAuth(sshKeyPath string) (ssh.Signer, error) {
}
signer, err := ssh.ParsePrivateKey(buf)
if err != nil {
return nil, err
fmt.Print("Enter ssh key passphrase: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return nil, err
}
signer, err := sshkeys.ParseEncryptedPrivateKey(buf, bytePassword)
if err != nil {
return nil, err
}
return signer, nil
}
return signer, err
}
@ -242,10 +296,13 @@ func (s *ScalewayClient) CopyImageToInstance(instanceID, path, sshKeyPath string
_, base := filepath.Split(path)
s.fileName = base
server, err := s.api.GetServer(instanceID)
serverResp, err := s.instanceAPI.GetServer(&instance.GetServerRequest{
ServerID: instanceID,
})
if err != nil {
return err
}
server := serverResp.Server
signer, err := getSSHAuth(sshKeyPath)
if err != nil {
@ -260,7 +317,7 @@ func (s *ScalewayClient) CopyImageToInstance(instanceID, path, sshKeyPath string
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO validate server before?
}
client, err := ssh.Dial("tcp", server.PublicAddress.IP+":22", s.sshConfig) // TODO remove hardocoded port?
client, err := ssh.Dial("tcp", server.PublicIP.Address.String()+":22", s.sshConfig) // TODO remove hardocoded port?
if err != nil {
return err
}
@ -303,12 +360,16 @@ func (s *ScalewayClient) CopyImageToInstance(instanceID, path, sshKeyPath string
// WriteImageToVolume does a dd command on the remote instance via ssh
func (s *ScalewayClient) WriteImageToVolume(instanceID, deviceName string) error {
server, err := s.api.GetServer(instanceID)
serverResp, err := s.instanceAPI.GetServer(&instance.GetServerRequest{
ServerID: instanceID,
})
if err != nil {
return err
}
client, err := ssh.Dial("tcp", server.PublicAddress.IP+":22", s.sshConfig) // TODO remove hardcoded port + use the same dial as before?
server := serverResp.Server
client, err := ssh.Dial("tcp", server.PublicIP.Address.String()+":22", s.sshConfig) // TODO remove hardcoded port + use the same dial as before?
if err != nil {
return err
}
@ -350,14 +411,12 @@ func (s *ScalewayClient) WriteImageToVolume(instanceID, deviceName string) error
// TerminateInstance terminates the instance and wait for termination
func (s *ScalewayClient) TerminateInstance(instanceID string) error {
server, err := s.api.GetServer(instanceID)
if err != nil {
return err
}
log.Debugf("Shutting down server %s", instanceID)
err = s.api.PostServerAction(server.Identifier, "poweroff")
_, err := s.instanceAPI.ServerAction(&instance.ServerActionRequest{
ServerID: instanceID,
Action: instance.ServerActionPoweroff,
})
if err != nil {
return err
}
@ -365,19 +424,24 @@ func (s *ScalewayClient) TerminateInstance(instanceID string) error {
// code taken from scaleway-cli
time.Sleep(10 * time.Second)
var currentState string
var currentState instance.ServerState
log.Debugf("Waiting for server to shutdown")
for {
server, err = s.api.GetServer(instanceID)
serverResp, err := s.instanceAPI.GetServer(&instance.GetServerRequest{
ServerID: instanceID,
})
if err != nil {
return err
}
server := serverResp.Server
if currentState != server.State {
currentState = server.State
}
if server.State == "stopped" {
if server.State == instance.ServerStateStopped {
break
}
time.Sleep(1 * time.Second)
@ -387,51 +451,76 @@ func (s *ScalewayClient) TerminateInstance(instanceID string) error {
// CreateScalewayImage creates the image and delete old image and snapshot if same name
func (s *ScalewayClient) CreateScalewayImage(instanceID, volumeID, name string) error {
oldImage, err := s.api.GetImageID(name, "x86_64")
oldImageID, err := s.getImageID(name, defaultScalewayCommercialType, defaultArch)
if err == nil {
err = s.api.DeleteImage(oldImage.Identifier)
log.Debugf("deleting image %s", oldImageID)
err = s.instanceAPI.DeleteImage(&instance.DeleteImageRequest{
ImageID: oldImageID,
})
if err != nil {
return err
}
}
oldSnapshot, err := s.api.GetSnapshotID(name)
oldSnapshotsResp, err := s.instanceAPI.ListSnapshots(&instance.ListSnapshotsRequest{
Name: &name,
}, scw.WithAllPages())
if err == nil {
err := s.api.DeleteSnapshot(oldSnapshot)
if err != nil {
return err
for _, oldSnapshot := range oldSnapshotsResp.Snapshots {
log.Debugf("deleting snapshot %s", oldSnapshot.ID)
err = s.instanceAPI.DeleteSnapshot(&instance.DeleteSnapshotRequest{
SnapshotID: oldSnapshot.ID,
})
if err != nil {
return err
}
}
}
snapshotID, err := s.api.PostSnapshot(volumeID, name)
log.Debugf("creating snapshot %s with volume %s", name, volumeID)
snapshotResp, err := s.instanceAPI.CreateSnapshot(&instance.CreateSnapshotRequest{
VolumeID: volumeID,
Name: name,
})
if err != nil {
return err
}
imageID, err := s.api.PostImage(snapshotID, name, "", "x86_64") // TODO remove hardcoded arch
log.Debugf("creating image %s with snapshot %s", name, snapshotResp.Snapshot.ID)
imageResp, err := s.instanceAPI.CreateImage(&instance.CreateImageRequest{
Name: name,
Arch: instance.Arch(defaultArch),
RootVolume: snapshotResp.Snapshot.ID,
})
if err != nil {
return err
}
log.Infof("Image %s with ID %s created", name, imageID)
log.Infof("Image %s with ID %s created", name, imageResp.Image.ID)
return nil
}
// DeleteInstanceAndVolumes deletes the instance and the volumes attached
func (s *ScalewayClient) DeleteInstanceAndVolumes(instanceID string) error {
server, err := s.api.GetServer(instanceID)
serverResp, err := s.instanceAPI.GetServer(&instance.GetServerRequest{
ServerID: instanceID,
})
if err != nil {
return err
}
err = s.api.DeleteServer(instanceID)
err = s.instanceAPI.DeleteServer(&instance.DeleteServerRequest{
ServerID: instanceID,
})
if err != nil {
return err
}
for _, volume := range server.Volumes {
err = s.api.DeleteVolume(volume.Identifier)
for _, volume := range serverResp.Server.Volumes {
err = s.instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
VolumeID: volume.ID,
})
if err != nil {
return err
}
@ -445,33 +534,38 @@ func (s *ScalewayClient) DeleteInstanceAndVolumes(instanceID string) error {
// CreateLinuxkitInstance creates an instance with the given linuxkit image
func (s *ScalewayClient) CreateLinuxkitInstance(instanceName, imageName, instanceType string) (string, error) {
// get the image ID
image, err := s.api.GetImageID(imageName, "x86_64") // TODO fix arch and use from args
imageResp, err := s.instanceAPI.ListImages(&instance.ListImagesRequest{
Name: &imageName,
})
if err != nil {
return "", err
}
imageID := image.Identifier
if len(imageResp.Images) != 1 {
return "", fmt.Errorf("Image %s not found or found multiple times", imageName)
}
imageID := imageResp.Images[0].ID
var serverDefinition types.ScalewayServerDefinition
serverDefinition.Name = instanceName
serverDefinition.Image = &imageID
serverDefinition.CommercialType = instanceType
serverDefinition.BootType = "local"
log.Debugf("Creating volume on Scaleway")
log.Debugf("Creating server %s on Scaleway", serverDefinition.Name)
serverID, err := s.api.PostServer(serverDefinition)
log.Debugf("Creating server %s on Scaleway", instanceName)
serverResp, err := s.instanceAPI.CreateServer(&instance.CreateServerRequest{
Name: instanceName,
DynamicIPRequired: true,
CommercialType: instanceType,
Image: imageID,
BootType: instance.ServerBootTypeLocal,
})
if err != nil {
return "", err
}
return serverID, nil
return serverResp.Server.ID, nil
}
// BootInstance boots the specified instance, and don't wait
func (s *ScalewayClient) BootInstance(instanceID string) error {
err := s.api.PostServerAction(instanceID, "poweron")
_, err := s.instanceAPI.ServerAction(&instance.ServerActionRequest{
ServerID: instanceID,
Action: instance.ServerActionPoweron,
})
if err != nil {
return err
}
@ -481,7 +575,7 @@ func (s *ScalewayClient) BootInstance(instanceID string) error {
// ConnectSerialPort connects to the serial port of the instance
func (s *ScalewayClient) ConnectSerialPort(instanceID string) error {
var gottyURL string
switch s.region {
switch s.zone {
case "par1":
gottyURL = "https://tty-par1.scaleway.com/v2/"
case "ams1":
@ -490,7 +584,7 @@ func (s *ScalewayClient) ConnectSerialPort(instanceID string) error {
return errors.New("Instance have no region")
}
fullURL := fmt.Sprintf("%s?arg=%s&arg=%s", gottyURL, s.api.Token, instanceID)
fullURL := fmt.Sprintf("%s?arg=%s&arg=%s", gottyURL, s.secretKey, instanceID)
log.Debugf("Connection to %s", fullURL)
gottyClient, err := gotty.NewClient(fullURL)

View File

@ -10,6 +10,7 @@ github.com/containerd/containerd v1.1.2
github.com/containerd/continuity d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371
github.com/creack/goselect 58854f77ee8d858ce751b0a9bcc5533fef7bfa9e
github.com/davecgh/go-spew v1.1.0
github.com/dchest/bcrypt_pbkdf 83f37f9c154a678179d11e218bff73ebe5717f99
github.com/dgrijalva/jwt-go 6c8dedd55f8a2e41f605de6d5d66e51ed1f299fc
github.com/docker/cli v18.06.0-ce
github.com/docker/distribution 83389a148052d74ac602f5f1d62f86ff2f3c4aa5
@ -54,7 +55,8 @@ github.com/radu-matei/azure-sdk-for-go 3b12823551999669c9a325a32472508e0af7978e
github.com/radu-matei/azure-vhd-utils e52754d5569d2a643a7775f72ff2a6cf524f4c25
github.com/renstrom/fuzzysearch 7a8f9a1c4bed53899ecd512daeaf8207cc454156
github.com/rn/iso9660wrap baf8d62ad3155152b488d5ff9d4f2b9bb0d6986a
github.com/scaleway/go-scaleway f8c3b31c65867b4cb96b3bd41e574c33fd1d69ae
github.com/ScaleFT/sshkeys 82451a80368171b074c7129d43b47fc2773f6e9f
github.com/scaleway/scaleway-sdk-go 20b731586975c078d9c2d7dd0002127e9e9cdef2
github.com/sirupsen/logrus v1.0.3
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.1

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,10 @@
sshkeys
Copyright 2017 ScaleFT, Inc
This product includes software developed at ScaleFT, Inc.
(https://www.scaleft.com/).
Portions of this software are derived from
https://github.com/golang/crypto/blob/master/ssh/keys.go
Copyright (c) 2009 The Go Authors. All rights reserved.

View File

@ -0,0 +1,14 @@
# sshkeys
[![GoDoc](https://godoc.org/github.com/ScaleFT/sshkeys?status.svg)](https://godoc.org/github.com/ScaleFT/sshkeys)
[![Build Status](https://travis-ci.org/ScaleFT/sshkeys.svg?branch=master)](https://travis-ci.org/ScaleFT/sshkeys)
`sshkeys` provides utilities for parsing and marshalling cryptographic keys used for SSH, in both cleartext and encrypted formats.
[ssh.ParseRawPrivateKey](https://godoc.org/golang.org/x/crypto/ssh#ParseRawPrivateKey) only supports parsing a subset of the formats `sshkeys` supports, does not support parsing encrypted private keys, and does not support marshalling.
## Supported Formats
* OpenSSH's [PROTOCOL.key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key) for RSA and ED25519 keys.
* OpenSSH version >= 7.6 using aes256-ctr encryption
* "Classic" PEM containing RSA (PKCS#1), DSA (OpenSSL), and ECDSA private keys.

View File

@ -0,0 +1,275 @@
package sshkeys
import (
"crypto/aes"
"crypto/cipher"
"crypto/dsa"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"math/big"
mrand "math/rand"
"github.com/dchest/bcrypt_pbkdf"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
)
// Format of private key to use when Marshaling.
type Format int
const (
// FormatOpenSSHv1 encodes a private key using OpenSSH's PROTOCOL.key format: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
FormatOpenSSHv1 Format = iota
// FormatClassicPEM encodes private keys in PEM, with a key-specific encoding, as used by OpenSSH.
FormatClassicPEM
)
// MarshalOptions provides the Marshal function format and encryption options.
type MarshalOptions struct {
// Passphrase to encrypt private key with, if nil, the key will not be encrypted.
Passphrase []byte
// Format to encode the private key in.
Format Format
}
// Marshal converts a private key into an optionally encrypted format.
func Marshal(pk interface{}, opts *MarshalOptions) ([]byte, error) {
switch opts.Format {
case FormatOpenSSHv1:
return marshalOpenssh(pk, opts)
case FormatClassicPEM:
return marshalPem(pk, opts)
default:
return nil, fmt.Errorf("sshkeys: invalid format %d", opts.Format)
}
}
func marshalPem(pk interface{}, opts *MarshalOptions) ([]byte, error) {
var err error
var plain []byte
var pemType string
switch key := pk.(type) {
case *rsa.PrivateKey:
pemType = "RSA PRIVATE KEY"
plain = x509.MarshalPKCS1PrivateKey(key)
case *ecdsa.PrivateKey:
pemType = "EC PRIVATE KEY"
plain, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
case *dsa.PrivateKey:
pemType = "DSA PRIVATE KEY"
plain, err = marshalDSAPrivateKey(key)
if err != nil {
return nil, err
}
case *ed25519.PrivateKey:
return nil, fmt.Errorf("sshkeys: ed25519 keys must be marshaled with FormatOpenSSHv1")
default:
return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
}
if len(opts.Passphrase) > 0 {
block, err := x509.EncryptPEMBlock(rand.Reader, pemType, plain, opts.Passphrase, x509.PEMCipherAES128)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(block), nil
}
return pem.EncodeToMemory(&pem.Block{
Type: pemType,
Bytes: plain,
}), nil
}
type dsaOpenssl struct {
Version int
P *big.Int
Q *big.Int
G *big.Int
Pub *big.Int
Priv *big.Int
}
// https://github.com/golang/crypto/blob/master/ssh/keys.go#L793-L804
func marshalDSAPrivateKey(pk *dsa.PrivateKey) ([]byte, error) {
k := dsaOpenssl{
Version: 0,
P: pk.P,
Q: pk.Q,
G: pk.G,
Pub: pk.Y,
Priv: pk.X,
}
return asn1.Marshal(k)
}
const opensshv1Magic = "openssh-key-v1"
type opensshHeader struct {
CipherName string
KdfName string
KdfOpts string
NumKeys uint32
PubKey string
PrivKeyBlock string
}
type opensshKey struct {
Check1 uint32
Check2 uint32
Keytype string
Rest []byte `ssh:"rest"`
}
type opensshRsa struct {
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int
P *big.Int
Q *big.Int
Comment string
Pad []byte `ssh:"rest"`
}
type opensshED25519 struct {
Pub []byte
Priv []byte
Comment string
Pad []byte `ssh:"rest"`
}
func padBytes(data []byte, blocksize int) []byte {
if blocksize != 0 {
var i byte
for i = byte(1); len(data)%blocksize != 0; i++ {
data = append(data, i&0xFF)
}
}
return data
}
func marshalOpenssh(pk interface{}, opts *MarshalOptions) ([]byte, error) {
var blocksize int
var keylen int
out := opensshHeader{
CipherName: "none",
KdfName: "none",
KdfOpts: "",
NumKeys: 1,
PubKey: "",
}
if len(opts.Passphrase) > 0 {
out.CipherName = "aes256-cbc"
out.KdfName = "bcrypt"
keylen = keySizeAES256
blocksize = aes.BlockSize
}
check := mrand.Uint32()
pk1 := opensshKey{
Check1: check,
Check2: check,
}
switch key := pk.(type) {
case *rsa.PrivateKey:
k := &opensshRsa{
N: key.N,
E: big.NewInt(int64(key.E)),
D: key.D,
Iqmp: key.Precomputed.Qinv,
P: key.Primes[0],
Q: key.Primes[1],
Comment: "",
}
data := ssh.Marshal(k)
pk1.Keytype = ssh.KeyAlgoRSA
pk1.Rest = data
publicKey, err := ssh.NewPublicKey(&key.PublicKey)
if err != nil {
return nil, err
}
out.PubKey = string(publicKey.Marshal())
case ed25519.PrivateKey:
k := opensshED25519{
Pub: key.Public().(ed25519.PublicKey),
Priv: key,
}
data := ssh.Marshal(k)
pk1.Keytype = ssh.KeyAlgoED25519
pk1.Rest = data
publicKey, err := ssh.NewPublicKey(key.Public())
if err != nil {
return nil, err
}
out.PubKey = string(publicKey.Marshal())
default:
return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
}
if len(opts.Passphrase) > 0 {
rounds := 16
ivlen := blocksize
salt := make([]byte, blocksize)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
kdfdata, err := bcrypt_pbkdf.Key(opts.Passphrase, salt, rounds, keylen+ivlen)
if err != nil {
return nil, err
}
iv := kdfdata[keylen : ivlen+keylen]
aeskey := kdfdata[0:keylen]
block, err := aes.NewCipher(aeskey)
if err != nil {
return nil, err
}
pkblock := padBytes(ssh.Marshal(pk1), blocksize)
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(pkblock, pkblock)
out.PrivKeyBlock = string(pkblock)
var opts struct {
Salt []byte
Rounds uint32
}
opts.Salt = salt
opts.Rounds = uint32(rounds)
out.KdfOpts = string(ssh.Marshal(&opts))
} else {
out.PrivKeyBlock = string(ssh.Marshal(pk1))
}
outBytes := []byte(opensshv1Magic)
outBytes = append(outBytes, 0)
outBytes = append(outBytes, ssh.Marshal(out)...)
block := &pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: outBytes,
}
return pem.EncodeToMemory(block), nil
}

View File

@ -0,0 +1,244 @@
// Portions of this file are based on https://github.com/golang/crypto/blob/master/ssh/keys.go
//
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sshkeys
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"math/big"
"strings"
"github.com/dchest/bcrypt_pbkdf"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
)
// ErrIncorrectPassword is returned when the supplied passphrase was not correct for an encrypted private key.
var ErrIncorrectPassword = errors.New("sshkeys: Invalid Passphrase")
const keySizeAES256 = 32
// ParseEncryptedPrivateKey returns a Signer from an encrypted private key. It supports
// the same keys as ParseEncryptedRawPrivateKey.
func ParseEncryptedPrivateKey(data []byte, passphrase []byte) (ssh.Signer, error) {
key, err := ParseEncryptedRawPrivateKey(data, passphrase)
if err != nil {
return nil, err
}
return ssh.NewSignerFromKey(key)
}
// ParseEncryptedRawPrivateKey returns a private key from an encrypted private key. It
// supports RSA (PKCS#1 or OpenSSH), DSA (OpenSSL), and ECDSA private keys.
//
// ErrIncorrectPassword will be returned if the supplied passphrase is wrong,
// but some formats like RSA in PKCS#1 detecting a wrong passphrase is difficult,
// and other parse errors may be returned.
func ParseEncryptedRawPrivateKey(data []byte, passphrase []byte) (interface{}, error) {
var err error
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("no PEM block found")
}
if x509.IsEncryptedPEMBlock(block) {
data, err = x509.DecryptPEMBlock(block, passphrase)
if err == x509.IncorrectPasswordError {
return nil, ErrIncorrectPassword
}
if err != nil {
return nil, err
}
} else {
data = block.Bytes
}
switch block.Type {
case "RSA PRIVATE KEY":
pk, err := x509.ParsePKCS1PrivateKey(data)
if err != nil {
// The Algos for PEM Encryption do not include strong message authentication,
// so sometimes DecryptPEMBlock works, but ParsePKCS1PrivateKey fails with an asn1 error.
// We are just catching the most common prefix here...
if strings.HasPrefix(err.Error(), "asn1: structure error") {
return nil, ErrIncorrectPassword
}
return nil, err
}
return pk, nil
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(data)
case "DSA PRIVATE KEY":
return ssh.ParseDSAPrivateKey(data)
case "OPENSSH PRIVATE KEY":
return parseOpenSSHPrivateKey(data, passphrase)
default:
return nil, fmt.Errorf("sshkeys: unsupported key type %q", block.Type)
}
}
func parseOpenSSHPrivateKey(data []byte, passphrase []byte) (interface{}, error) {
magic := append([]byte(opensshv1Magic), 0)
if !bytes.Equal(magic, data[0:len(magic)]) {
return nil, errors.New("sshkeys: invalid openssh private key format")
}
remaining := data[len(magic):]
w := opensshHeader{}
if err := ssh.Unmarshal(remaining, &w); err != nil {
return nil, err
}
if w.NumKeys != 1 {
return nil, fmt.Errorf("sshkeys: NumKeys must be 1: %d", w.NumKeys)
}
var privateKeyBytes []byte
var encrypted bool
switch {
// OpenSSH supports bcrypt KDF w/ AES256-CBC or AES256-CTR mode
case w.KdfName == "bcrypt" && w.CipherName == "aes256-cbc":
iv, block, err := extractBcryptIvBlock(passphrase, w)
if err != nil {
return nil, err
}
cbc := cipher.NewCBCDecrypter(block, iv)
privateKeyBytes = []byte(w.PrivKeyBlock)
cbc.CryptBlocks(privateKeyBytes, privateKeyBytes)
encrypted = true
case w.KdfName == "bcrypt" && w.CipherName == "aes256-ctr":
iv, block, err := extractBcryptIvBlock(passphrase, w)
if err != nil {
return nil, err
}
stream := cipher.NewCTR(block, iv)
privateKeyBytes = []byte(w.PrivKeyBlock)
stream.XORKeyStream(privateKeyBytes, privateKeyBytes)
encrypted = true
case w.KdfName == "none" && w.CipherName == "none":
privateKeyBytes = []byte(w.PrivKeyBlock)
default:
return nil, fmt.Errorf("sshkeys: unknown Cipher/KDF: %s:%s", w.CipherName, w.KdfName)
}
pk1 := opensshKey{}
if err := ssh.Unmarshal(privateKeyBytes, &pk1); err != nil {
if encrypted {
return nil, ErrIncorrectPassword
}
return nil, err
}
if pk1.Check1 != pk1.Check2 {
return nil, ErrIncorrectPassword
}
// we only handle ed25519 and rsa keys currently
switch pk1.Keytype {
case ssh.KeyAlgoRSA:
// https://github.com/openssh/openssh-portable/blob/V_7_4_P1/sshkey.c#L2760-L2773
key := opensshRsa{}
err := ssh.Unmarshal(pk1.Rest, &key)
if err != nil {
return nil, err
}
for i, b := range key.Pad {
if int(b) != i+1 {
return nil, errors.New("sshkeys: padding not as expected")
}
}
pk := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: key.N,
E: int(key.E.Int64()),
},
D: key.D,
Primes: []*big.Int{key.P, key.Q},
}
err = pk.Validate()
if err != nil {
return nil, err
}
pk.Precompute()
return pk, nil
case ssh.KeyAlgoED25519:
key := opensshED25519{}
err := ssh.Unmarshal(pk1.Rest, &key)
if err != nil {
return nil, err
}
if len(key.Priv) != ed25519.PrivateKeySize {
return nil, errors.New("sshkeys: private key unexpected length")
}
for i, b := range key.Pad {
if int(b) != i+1 {
return nil, errors.New("sshkeys: padding not as expected")
}
}
pk := ed25519.PrivateKey(make([]byte, ed25519.PrivateKeySize))
copy(pk, key.Priv)
return pk, nil
default:
return nil, errors.New("sshkeys: unhandled key type")
}
}
func extractBcryptIvBlock(passphrase []byte, w opensshHeader) ([]byte, cipher.Block, error) {
cipherKeylen := keySizeAES256
cipherIvLen := aes.BlockSize
var opts struct {
Salt []byte
Rounds uint32
}
if err := ssh.Unmarshal([]byte(w.KdfOpts), &opts); err != nil {
return nil, nil, err
}
kdfdata, err := bcrypt_pbkdf.Key(passphrase, opts.Salt, int(opts.Rounds), cipherKeylen+cipherIvLen)
if err != nil {
return nil, nil, err
}
iv := kdfdata[cipherKeylen : cipherIvLen+cipherKeylen]
aeskey := kdfdata[0:cipherKeylen]
block, err := aes.NewCipher(aeskey)
if err != nil {
return nil, nil, err
}
return iv, block, nil
}

View File

@ -0,0 +1,27 @@
Copyright (c) 2014 Dmitry Chestnykh <dmitry@codingrobots.com>
Copyright (c) 2010 The Go Authors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,21 @@
Go implementation of bcrypt_pbkdf(3) from OpenBSD
(a variant of PBKDF2 with bcrypt-based PRF).
USAGE
func Key(password, salt []byte, rounds, keyLen int) ([]byte, error)
Key derives a key from the password, salt and rounds count, returning a
[]byte of length keyLen that can be used as cryptographic key.
Remember to get a good random salt of at least 16 bytes. Using a higher
rounds count will increase the cost of an exhaustive search but will also
make derivation proportionally slower.
REFERENCES
* http://www.tedunangst.com/flak/post/bcrypt-pbkdf
* http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libutil/bcrypt_pbkdf.c

View File

@ -0,0 +1,97 @@
// Copyright 2014 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package bcrypt_pbkdf implements password-based key derivation function based
// on bcrypt compatible with bcrypt_pbkdf(3) from OpenBSD.
package bcrypt_pbkdf
import (
"crypto/sha512"
"errors"
// NOTE! Requires blowfish package version from Aug 1, 2014 or later.
// Will produce incorrect results if the package is older.
// See commit message for details: http://goo.gl/wx6g8O
"golang.org/x/crypto/blowfish"
)
// Key derives a key from the password, salt and rounds count, returning a
// []byte of length keyLen that can be used as cryptographic key.
//
// Remember to get a good random salt of at least 16 bytes. Using a higher
// rounds count will increase the cost of an exhaustive search but will also
// make derivation proportionally slower.
func Key(password, salt []byte, rounds, keyLen int) ([]byte, error) {
if rounds < 1 {
return nil, errors.New("bcrypt_pbkdf: number of rounds is too small")
}
if len(password) == 0 {
return nil, errors.New("bcrypt_pbkdf: empty password")
}
if len(salt) == 0 || len(salt) > 1<<20 {
return nil, errors.New("bcrypt_pbkdf: bad salt length")
}
if keyLen > 1024 {
return nil, errors.New("bcrypt_pbkdf: keyLen is too large")
}
var shapass, shasalt [sha512.Size]byte
var out, tmp [32]byte
var cnt [4]byte
numBlocks := (keyLen + len(out) - 1) / len(out)
key := make([]byte, numBlocks*len(out))
h := sha512.New()
h.Write(password)
h.Sum(shapass[:0])
for block := 1; block <= numBlocks; block++ {
h.Reset()
h.Write(salt)
cnt[0] = byte(block >> 24)
cnt[1] = byte(block >> 16)
cnt[2] = byte(block >> 8)
cnt[3] = byte(block)
h.Write(cnt[:])
bcryptHash(tmp[:], shapass[:], h.Sum(shasalt[:0]))
copy(out[:], tmp[:])
for i := 2; i <= rounds; i++ {
h.Reset()
h.Write(tmp[:])
bcryptHash(tmp[:], shapass[:], h.Sum(shasalt[:0]))
for j := 0; j < len(out); j++ {
out[j] ^= tmp[j]
}
}
for i, v := range out {
key[i*numBlocks+(block-1)] = v
}
}
return key[:keyLen], nil
}
var magic = []byte("OxychromaticBlowfishSwatDynamite")
func bcryptHash(out, shapass, shasalt []byte) {
c, err := blowfish.NewSaltedCipher(shapass, shasalt)
if err != nil {
panic(err)
}
for i := 0; i < 64; i++ {
blowfish.ExpandKey(shasalt, c)
blowfish.ExpandKey(shapass, c)
}
copy(out[:], magic)
for i := 0; i < 32; i += 8 {
for j := 0; j < 64; j++ {
c.Encrypt(out[i:i+8], out[i:i+8])
}
}
// Swap bytes due to different endianness.
for i := 0; i < 32; i += 4 {
out[i+3], out[i+2], out[i+1], out[i] = out[i], out[i+1], out[i+2], out[i+3]
}
}

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Manfred Touron
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,171 +0,0 @@
# AnonUUID
[![Build Status](https://travis-ci.org/moul/anonuuid.svg)](https://travis-ci.org/moul/anonuuid)
[![GoDoc](https://godoc.org/github.com/moul/anonuuid?status.svg)](https://godoc.org/github.com/moul/anonuuid)
[![Coverage Status](https://coveralls.io/repos/moul/anonuuid/badge.svg?branch=master&service=github)](https://coveralls.io/github/moul/anonuuid?branch=master)
:wrench: Anonymize UUIDs outputs (written in Golang)
![AnonUUID Logo](https://raw.githubusercontent.com/moul/anonuuid/master/assets/anonuuid.png)
**anonuuid** anonymize an input string by replacing all UUIDs by an anonymized
new one.
The fake UUIDs are cached, so if AnonUUID encounter the same real UUIDs multiple
times, the translation will be the same.
## Usage
```console
$ anonuuid --help
NAME:
anonuuid - Anonymize UUIDs outputs
USAGE:
anonuuid [global options] command [command options] [arguments...]
VERSION:
1.0.0-dev
AUTHOR(S):
Manfred Touron <https://github.com/moul>
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--hexspeak Generate hexspeak style fake UUIDs
--random, -r Generate random fake UUIDs
--keep-beginning Keep first part of the UUID unchanged
--keep-end Keep last part of the UUID unchanged
--prefix, -p Prefix generated UUIDs
--suffix Suffix generated UUIDs
--help, -h show help
--version, -v print the version
```
## Example
Replace all UUIDs and cache the correspondance.
```command
$ anonuuid git:(master) ✗ cat <<EOF | anonuuid
VOLUMES_0_SERVER_ID=15573749-c89d-41dd-a655-16e79bed52e0
VOLUMES_0_SERVER_NAME=hello
VOLUMES_0_ID=c245c3cb-3336-4567-ada1-70cb1fe4eefe
VOLUMES_0_SIZE=50000000000
ORGANIZATION=fe1e54e8-d69d-4f7c-a9f1-42069e03da31
TEST=15573749-c89d-41dd-a655-16e79bed52e0
EOF
VOLUMES_0_SERVER_ID=00000000-0000-0000-0000-000000000000
VOLUMES_0_SERVER_NAME=hello
VOLUMES_0_ID=11111111-1111-1111-1111-111111111111
VOLUMES_0_SIZE=50000000000
ORGANIZATION=22222222-2222-2222-2222-222222222222
TEST=00000000-0000-0000-0000-000000000000
```
---
Inline
```command
$ echo 'VOLUMES_0_SERVER_ID=15573749-c89d-41dd-a655-16e79bed52e0 VOLUMES_0_SERVER_NAME=bitrig1 VOLUMES_0_ID=c245c3cb-3336-4567-ada1-70cb1fe4eefe VOLUMES_0_SIZE=50000000000 ORGANIZATION=fe1e54e8-d69d-4f7c-a9f1-42069e03da31 TEST=15573749-c89d-41dd-a655-16e79bed52e0' | ./anonuuid
VOLUMES_0_SERVER_ID=00000000-0000-0000-0000-000000000000 VOLUMES_0_SERVER_NAME=bitrig1 VOLUMES_0_ID=11111111-1111-1111-1111-111111111111 VOLUMES_0_SIZE=50000000000 ORGANIZATION=22222222-2222-2222-2222-222222222222 TEST=00000000-0000-0000-0000-000000000000
```
---
```command
$ curl -s https://api.pathwar.net/achievements\?max_results\=2 | anonuuid | jq .
{
"_items": [
{
"_updated": "Thu, 30 Apr 2015 13:00:58 GMT",
"description": "You",
"_links": {
"self": {
"href": "achievements/00000000-0000-0000-0000-000000000000",
"title": "achievement"
}
},
"_created": "Thu, 30 Apr 2015 13:00:58 GMT",
"_id": "00000000-0000-0000-0000-000000000000",
"_etag": "b1e9f850accfcb952c58384db41d89728890a69f",
"name": "finish-20-levels"
},
{
"_updated": "Thu, 30 Apr 2015 13:01:07 GMT",
"description": "You",
"_links": {
"self": {
"href": "achievements/11111111-1111-1111-1111-111111111111",
"title": "achievement"
}
},
"_created": "Thu, 30 Apr 2015 13:01:07 GMT",
"_id": "11111111-1111-1111-1111-111111111111",
"_etag": "c346f5e1c4f7658f2dfc4124efa87aba909a9821",
"name": "buy-30-levels"
}
],
"_links": {
"self": {
"href": "achievements?max_results=2",
"title": "achievements"
},
"last": {
"href": "achievements?max_results=2&page=23",
"title": "last page"
},
"parent": {
"href": "/",
"title": "home"
},
"next": {
"href": "achievements?max_results=2&page=2",
"title": "next page"
}
},
"_meta": {
"max_results": 2,
"total": 46,
"page": 1
}
}
```
## Install
Using go
- `go get github.com/moul/anonuuid/...`
## Changelog
### [v1.1.0](https://github.com/moul/anonuuid/releases/tag/v1.1.0) (2018-04-02)
* Switch from `Godep` to `Glide`
* Add mutex to protect the cache field ([@QuentinPerez](https://github.com/QuentinPerez))
* Switch from `Party` to `Godep`
* Support of `--suffix=xxx`, `--keep-beginning` and `--keep-end` options ([#4](https://github.com/moul/anonuuid/issues/4))
* Using **party** to stabilize vendor package versions ([#8](https://github.com/moul/anonuuid/issues/8))
* Add homebrew package ([#6](https://github.com/moul/anonuuid/issues/6))
[full commits list](https://github.com/moul/anonuuid/compare/v1.0.0...master)
### [v1.0.0](https://github.com/moul/anonuuid/releases/tag/v1.0.0) (2015-10-07)
**Initial release**
#### Features
* Support of `--hexspeak` option
* Support of `--random` option
* Support of `--prefix` option
* Anonymize input stream
* Anonymize files
## License
MIT

View File

@ -1,229 +0,0 @@
package anonuuid
import (
"fmt"
"log"
"math/rand"
"regexp"
"strings"
"sync"
"time"
)
var (
// UUIDRegex is the regex used to find UUIDs in texts
UUIDRegex = "[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}"
)
// AnonUUID is the main structure, it contains the cache map and helpers
type AnonUUID struct {
cache map[string]string
guard sync.Mutex // cache guard
// Hexspeak flag will generate hexspeak style fake UUIDs
Hexspeak bool
// Random flag will generate random fake UUIDs
Random bool
// Prefix will be the beginning of all the generated UUIDs
Prefix string
// Suffix will be the end of all the generated UUIDs
Suffix string
// AllowNonUUIDInput tells FakeUUID to accept non UUID input string
AllowNonUUIDInput bool
// KeepBeginning tells FakeUUID to let the beginning of the UUID as it is
KeepBeginning bool
// KeepEnd tells FakeUUID to let the last part of the UUID as it is
KeepEnd bool
}
// Sanitize takes a string as input and return sanitized string
func (a *AnonUUID) Sanitize(input string) string {
r := regexp.MustCompile(UUIDRegex)
return r.ReplaceAllStringFunc(input, func(m string) string {
parts := r.FindStringSubmatch(m)
return a.FakeUUID(parts[0])
})
}
// FakeUUID takes a word (real UUID or standard string) and returns its corresponding (mapped) fakeUUID
func (a *AnonUUID) FakeUUID(input string) string {
if !a.AllowNonUUIDInput {
err := IsUUID(input)
if err != nil {
return "invaliduuid"
}
}
a.guard.Lock()
defer a.guard.Unlock()
if _, ok := a.cache[input]; !ok {
if a.KeepBeginning {
a.Prefix = input[:8]
}
if a.KeepEnd {
a.Suffix = input[36-12:]
}
if a.Prefix != "" {
matched, err := regexp.MatchString("^[a-z0-9]+$", a.Prefix)
if err != nil || !matched {
a.Prefix = "invalidprefix"
}
}
if a.Suffix != "" {
matched, err := regexp.MatchString("^[a-z0-9]+$", a.Suffix)
if err != nil || !matched {
a.Suffix = "invalsuffix"
}
}
var fakeUUID string
var err error
if a.Hexspeak {
fakeUUID, err = GenerateHexspeakUUID(len(a.cache))
} else if a.Random {
fakeUUID, err = GenerateRandomUUID(10)
} else {
fakeUUID, err = GenerateLenUUID(len(a.cache))
}
if err != nil {
log.Fatalf("Failed to generate an UUID: %v", err)
}
if a.Prefix != "" {
fakeUUID, err = PrefixUUID(a.Prefix, fakeUUID)
if err != nil {
panic(err)
}
}
if a.Suffix != "" {
fakeUUID, err = SuffixUUID(a.Suffix, fakeUUID)
if err != nil {
panic(err)
}
}
// FIXME: check for duplicates and retry
a.cache[input] = fakeUUID
}
return a.cache[input]
}
// New returns a prepared AnonUUID structure
func New() *AnonUUID {
return &AnonUUID{
cache: make(map[string]string),
Hexspeak: false,
Random: false,
}
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
// PrefixUUID returns a prefixed UUID
func PrefixUUID(prefix string, uuid string) (string, error) {
uuidLetters := uuid[:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36]
prefixedUUID, err := FormatUUID(prefix + uuidLetters)
if err != nil {
return "", err
}
return prefixedUUID, nil
}
// SuffixUUID returns a suffixed UUID
func SuffixUUID(suffix string, uuid string) (string, error) {
uuidLetters := uuid[:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36]
uuidLetters = uuidLetters[:32-len(suffix)] + suffix
suffixedUUID, err := FormatUUID(uuidLetters)
if err != nil {
return "", err
}
return suffixedUUID, nil
}
// IsUUID returns nil if the input is an UUID, else it returns an error
func IsUUID(input string) error {
matched, err := regexp.MatchString("^"+UUIDRegex+"$", input)
if err != nil {
return err
}
if !matched {
return fmt.Errorf("String '%s' is not a valid UUID", input)
}
return nil
}
// FormatUUID takes a string in input and return an UUID formatted string by repeating the string and placing dashes if necessary
func FormatUUID(part string) (string, error) {
if len(part) < 1 {
return "", fmt.Errorf("Empty UUID")
}
if len(part) < 32 {
part = strings.Repeat(part, 32)
}
if len(part) > 32 {
part = part[:32]
}
uuid := part[:8] + "-" + part[8:12] + "-1" + part[13:16] + "-" + part[16:20] + "-" + part[20:32]
err := IsUUID(uuid)
if err != nil {
return "", err
}
return uuid, nil
}
// GenerateRandomUUID returns an UUID based on random strings
func GenerateRandomUUID(length int) (string, error) {
var letters = []rune("abcdef0123456789")
b := make([]rune, length)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return FormatUUID(string(b))
}
// GenerateHexspeakUUID returns an UUID formatted string containing hexspeak words
func GenerateHexspeakUUID(i int) (string, error) {
if i < 0 {
i = -i
}
hexspeaks := []string{
"0ff1ce",
"31337",
"4b1d",
"badc0de",
"badcafe",
"badf00d",
"deadbabe",
"deadbeef",
"deadc0de",
"deadfeed",
"fee1bad",
}
return FormatUUID(hexspeaks[i%len(hexspeaks)])
}
// GenerateLenUUID returns an UUID formatted string based on an index number
func GenerateLenUUID(i int) (string, error) {
if i < 0 {
i = 2<<29 + i
}
return FormatUUID(fmt.Sprintf("%x", i))
}

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Peter Renström
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,43 +0,0 @@
# Fuzzy Search
[![Build Status](https://img.shields.io/travis/renstrom/fuzzysearch.svg?style=flat-square)](https://travis-ci.org/renstrom/fuzzysearch)
[![Godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/renstrom/fuzzysearch/fuzzy)
Inspired by _[bevacqua/fuzzysearch][1]_, a fuzzy matching library written in JavaScript. But contains some extras like ranking using _[Levenshtein distance][2]_ (see [`RankMatch()`](https://godoc.org/github.com/renstrom/fuzzysearch/fuzzy#RankMatch)) and finding matches in a list of words (see [`Find()`](https://godoc.org/github.com/renstrom/fuzzysearch/fuzzy#Find)).
Fuzzy searching allows for flexibly matching a string with partial input, useful for filtering data very quickly based on lightweight user input.
The current implementation uses the algorithm suggested by Mr. Aleph, a russian compiler engineer working at V8.
## Usage
```go
fuzzy.Match("twl", "cartwheel") // true
fuzzy.Match("cart", "cartwheel") // true
fuzzy.Match("cw", "cartwheel") // true
fuzzy.Match("ee", "cartwheel") // true
fuzzy.Match("art", "cartwheel") // true
fuzzy.Match("eeel", "cartwheel") // false
fuzzy.Match("dog", "cartwheel") // false
fuzzy.RankMatch("kitten", "sitting") // 3
words := []string{"cartwheel", "foobar", "wheel", "baz"}
fuzzy.Find("whl", words) // [cartwheel wheel]
fuzzy.RankFind("whl", words) // [{whl cartwheel 6} {whl wheel 2}]
```
You can sort the result of a `fuzzy.RankFind()` call using the [`sort`](https://golang.org/pkg/sort/) package in the standard library:
```go
matches := fuzzy.RankFind("whl", words) // [{whl cartwheel 6} {whl wheel 2}]
sort.Sort(matches) // [{whl wheel 2} {whl cartwheel 6}]
```
## License
MIT
[1]: https://github.com/bevacqua/fuzzysearch
[2]: http://en.wikipedia.org/wiki/Levenshtein_distance

View File

@ -1,167 +0,0 @@
// Fuzzy searching allows for flexibly matching a string with partial input,
// useful for filtering data very quickly based on lightweight user input.
package fuzzy
import (
"unicode"
"unicode/utf8"
)
var noop = func(r rune) rune { return r }
// Match returns true if source matches target using a fuzzy-searching
// algorithm. Note that it doesn't implement Levenshtein distance (see
// RankMatch instead), but rather a simplified version where there's no
// approximation. The method will return true only if each character in the
// source can be found in the target and occurs after the preceding matches.
func Match(source, target string) bool {
return match(source, target, noop)
}
// MatchFold is a case-insensitive version of Match.
func MatchFold(source, target string) bool {
return match(source, target, unicode.ToLower)
}
func match(source, target string, fn func(rune) rune) bool {
lenDiff := len(target) - len(source)
if lenDiff < 0 {
return false
}
if lenDiff == 0 && source == target {
return true
}
Outer:
for _, r1 := range source {
for i, r2 := range target {
if fn(r1) == fn(r2) {
target = target[i+utf8.RuneLen(r2):]
continue Outer
}
}
return false
}
return true
}
// Find will return a list of strings in targets that fuzzy matches source.
func Find(source string, targets []string) []string {
return find(source, targets, noop)
}
// FindFold is a case-insensitive version of Find.
func FindFold(source string, targets []string) []string {
return find(source, targets, unicode.ToLower)
}
func find(source string, targets []string, fn func(rune) rune) []string {
var matches []string
for _, target := range targets {
if match(source, target, fn) {
matches = append(matches, target)
}
}
return matches
}
// RankMatch is similar to Match except it will measure the Levenshtein
// distance between the source and the target and return its result. If there
// was no match, it will return -1.
// Given the requirements of match, RankMatch only needs to perform a subset of
// the Levenshtein calculation, only deletions need be considered, required
// additions and substitutions would fail the match test.
func RankMatch(source, target string) int {
return rank(source, target, noop)
}
// RankMatchFold is a case-insensitive version of RankMatch.
func RankMatchFold(source, target string) int {
return rank(source, target, unicode.ToLower)
}
func rank(source, target string, fn func(rune) rune) int {
lenDiff := len(target) - len(source)
if lenDiff < 0 {
return -1
}
if lenDiff == 0 && source == target {
return 0
}
runeDiff := 0
Outer:
for _, r1 := range source {
for i, r2 := range target {
if fn(r1) == fn(r2) {
target = target[i+utf8.RuneLen(r2):]
continue Outer
} else {
runeDiff++
}
}
return -1
}
// Count up remaining char
for len(target) > 0 {
target = target[utf8.RuneLen(rune(target[0])):]
runeDiff++
}
return runeDiff
}
// RankFind is similar to Find, except it will also rank all matches using
// Levenshtein distance.
func RankFind(source string, targets []string) Ranks {
var r Ranks
for _, target := range find(source, targets, noop) {
distance := LevenshteinDistance(source, target)
r = append(r, Rank{source, target, distance})
}
return r
}
// RankFindFold is a case-insensitive version of RankFind.
func RankFindFold(source string, targets []string) Ranks {
var r Ranks
for _, target := range find(source, targets, unicode.ToLower) {
distance := LevenshteinDistance(source, target)
r = append(r, Rank{source, target, distance})
}
return r
}
type Rank struct {
// Source is used as the source for matching.
Source string
// Target is the word matched against.
Target string
// Distance is the Levenshtein distance between Source and Target.
Distance int
}
type Ranks []Rank
func (r Ranks) Len() int {
return len(r)
}
func (r Ranks) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func (r Ranks) Less(i, j int) bool {
return r[i].Distance < r[j].Distance
}

View File

@ -1,43 +0,0 @@
package fuzzy
// LevenshteinDistance measures the difference between two strings.
// The Levenshtein distance between two words is the minimum number of
// single-character edits (i.e. insertions, deletions or substitutions)
// required to change one word into the other.
//
// This implemention is optimized to use O(min(m,n)) space and is based on the
// optimized C version found here:
// http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#C
func LevenshteinDistance(s, t string) int {
r1, r2 := []rune(s), []rune(t)
column := make([]int, len(r1)+1)
for y := 1; y <= len(r1); y++ {
column[y] = y
}
for x := 1; x <= len(r2); x++ {
column[0] = x
for y, lastDiag := 1, x-1; y <= len(r1); y++ {
oldDiag := column[y]
cost := 0
if r1[y-1] != r2[x-1] {
cost = 1
}
column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost)
lastDiag = oldDiag
}
}
return column[len(r1)]
}
func min(a, b, c int) int {
if a < b && a < c {
return a
} else if b < c {
return b
}
return c
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Scaleway
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,27 +0,0 @@
# Scaleway's API golang client
[![GoDoc](https://godoc.org/github.com/scaleway/go-scaleway?status.svg)](https://godoc.org/github.com/scaleway/go-scaleway)
This package contains facilities to play with the Scaleway API, it includes the following features:
- dedicated configuration file containing credentials to deal with the API
- caching to resolve UUIDs without contacting the API
## Links
- [API documentation](https://developer.scaleway.com)
- [Official Python SDK](https://github.com/scaleway/python-scaleway)
- Projects using this SDK
- https://github.com/scaleway/scaleway-cli
- https://github.com/scaleway/devhub
- https://github.com/scaleway/docker-machine-driver-scaleway
- https://github.com/huseyin/docker-machine-driver-scaleway
- https://github.com/scaleway-community/scaleway-ubuntu-coreos/blob/master/overlay/usr/local/update-firewall/scw-api/cache.go
- https://github.com/pulcy/quark
- https://github.com/hex-sh/terraform-provider-scaleway
- https://github.com/tscolari/bosh-scaleway-cpi
- Other **golang** clients
- https://github.com/lalyos/onlabs
- https://github.com/meatballhat/packer-builder-onlinelabs
- https://github.com/nlamirault/go-scaleway
- https://github.com/golang/build/blob/master/cmd/scaleway/scaleway.go

File diff suppressed because it is too large Load Diff

View File

@ -1,693 +0,0 @@
// Copyright (C) 2018 Scaleway. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.md file.
package cache
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/moul/anonuuid"
"github.com/renstrom/fuzzysearch/fuzzy"
"github.com/scaleway/go-scaleway/types"
)
const (
// CacheRegion permits to access at the region field
CacheRegion = iota
// CacheArch permits to access at the arch field
CacheArch
// CacheOwner permits to access at the owner field
CacheOwner
// CacheTitle permits to access at the title field
CacheTitle
// CacheMarketPlaceUUID is used to determine the UUID of local images
CacheMarketPlaceUUID
// CacheMaxfield is used to determine the size of array
CacheMaxfield
)
// ScalewayCache is used not to query the API to resolve full identifiers
type ScalewayCache struct {
// Images contains names of Scaleway images indexed by identifier
Images map[string][CacheMaxfield]string `json:"images"`
// Snapshots contains names of Scaleway snapshots indexed by identifier
Snapshots map[string][CacheMaxfield]string `json:"snapshots"`
// Volumes contains names of Scaleway volumes indexed by identifier
Volumes map[string][CacheMaxfield]string `json:"volumes"`
// Bootscripts contains names of Scaleway bootscripts indexed by identifier
Bootscripts map[string][CacheMaxfield]string `json:"bootscripts"`
// Servers contains names of Scaleway servers indexed by identifier
Servers map[string][CacheMaxfield]string `json:"servers"`
// Path is the path to the cache file
Path string `json:"-"`
// Modified tells if the cache needs to be overwritten or not
Modified bool `json:"-"`
// Lock allows ScalewayCache to be used concurrently
Lock sync.Mutex `json:"-"`
hookSave func()
}
// NewScalewayCache loads a per-user cache
func NewScalewayCache(hookSave func()) (*ScalewayCache, error) {
var cache ScalewayCache
cache.hookSave = hookSave
homeDir := os.Getenv("HOME") // *nix
if homeDir == "" { // Windows
homeDir = os.Getenv("USERPROFILE")
}
if homeDir == "" {
homeDir = "/tmp"
}
cachePath := filepath.Join(homeDir, ".scw-cache.db")
cache.Path = cachePath
_, err := os.Stat(cachePath)
if os.IsNotExist(err) {
cache.Clear()
return &cache, nil
} else if err != nil {
return nil, err
}
file, err := ioutil.ReadFile(cachePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(file, &cache)
if err != nil {
// fix compatibility with older version
if err = os.Remove(cachePath); err != nil {
return nil, err
}
cache.Clear()
return &cache, nil
}
if cache.Images == nil {
cache.Images = make(map[string][CacheMaxfield]string)
}
if cache.Snapshots == nil {
cache.Snapshots = make(map[string][CacheMaxfield]string)
}
if cache.Volumes == nil {
cache.Volumes = make(map[string][CacheMaxfield]string)
}
if cache.Servers == nil {
cache.Servers = make(map[string][CacheMaxfield]string)
}
if cache.Bootscripts == nil {
cache.Bootscripts = make(map[string][CacheMaxfield]string)
}
return &cache, nil
}
// Clear removes all information from the cache
func (c *ScalewayCache) Clear() {
c.Images = make(map[string][CacheMaxfield]string)
c.Snapshots = make(map[string][CacheMaxfield]string)
c.Volumes = make(map[string][CacheMaxfield]string)
c.Bootscripts = make(map[string][CacheMaxfield]string)
c.Servers = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// Flush flushes the cache database
func (c *ScalewayCache) Flush() error {
return os.Remove(c.Path)
}
// Save atomically overwrites the current cache database
func (c *ScalewayCache) Save() error {
c.Lock.Lock()
defer c.Lock.Unlock()
c.hookSave()
if c.Modified {
file, err := ioutil.TempFile(filepath.Dir(c.Path), filepath.Base(c.Path))
if err != nil {
return err
}
if err := json.NewEncoder(file).Encode(c); err != nil {
file.Close()
os.Remove(file.Name())
return err
}
file.Close()
if err := os.Rename(file.Name(), c.Path); err != nil {
os.Remove(file.Name())
return err
}
}
return nil
}
// ComputeRankMatch fills `ScalewayResolverResult.RankMatch` with its `fuzzy` score
func ComputeRankMatch(s *types.ScalewayResolverResult, needle string) {
s.Needle = needle
s.RankMatch = fuzzy.RankMatch(needle, s.Name)
}
// LookUpImages attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) (types.ScalewayResolverResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
var res types.ScalewayResolverResults
var exactMatches types.ScalewayResolverResults
if acceptUUID && anonuuid.IsUUID(needle) == nil {
if fields, ok := c.Images[needle]; ok {
entry, err := types.NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierImage)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
needle = regexp.MustCompile(`^user/`).ReplaceAllString(needle, "")
// FIXME: if 'user/' is in needle, only watch for a user image
nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*"))
for identifier, fields := range c.Images {
if fields[CacheTitle] == needle {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierImage)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
exactMatches = append(exactMatches, entry)
}
if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierImage)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
} else if strings.HasPrefix(fields[CacheMarketPlaceUUID], needle) || nameRegex.MatchString(fields[CacheMarketPlaceUUID]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierImage)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
if len(exactMatches) == 1 {
return exactMatches, nil
}
return removeDuplicatesResults(res), nil
}
// LookUpSnapshots attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) (types.ScalewayResolverResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
var res types.ScalewayResolverResults
var exactMatches types.ScalewayResolverResults
if acceptUUID && anonuuid.IsUUID(needle) == nil {
if fields, ok := c.Snapshots[needle]; ok {
entry, err := types.NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierSnapshot)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
needle = regexp.MustCompile(`^user/`).ReplaceAllString(needle, "")
nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*"))
for identifier, fields := range c.Snapshots {
if fields[CacheTitle] == needle {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierSnapshot)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
exactMatches = append(exactMatches, entry)
}
if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierSnapshot)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
if len(exactMatches) == 1 {
return exactMatches, nil
}
return removeDuplicatesResults(res), nil
}
// LookUpVolumes attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) (types.ScalewayResolverResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
var res types.ScalewayResolverResults
var exactMatches types.ScalewayResolverResults
if acceptUUID && anonuuid.IsUUID(needle) == nil {
if fields, ok := c.Volumes[needle]; ok {
entry, err := types.NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierVolume)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*"))
for identifier, fields := range c.Volumes {
if fields[CacheTitle] == needle {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierVolume)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
exactMatches = append(exactMatches, entry)
}
if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierVolume)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
if len(exactMatches) == 1 {
return exactMatches, nil
}
return removeDuplicatesResults(res), nil
}
// LookUpBootscripts attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) (types.ScalewayResolverResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
var res types.ScalewayResolverResults
var exactMatches types.ScalewayResolverResults
if acceptUUID && anonuuid.IsUUID(needle) == nil {
if fields, ok := c.Bootscripts[needle]; ok {
entry, err := types.NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierBootscript)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*"))
for identifier, fields := range c.Bootscripts {
if fields[CacheTitle] == needle {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierBootscript)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
exactMatches = append(exactMatches, entry)
}
if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierBootscript)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
if len(exactMatches) == 1 {
return exactMatches, nil
}
return removeDuplicatesResults(res), nil
}
// LookUpServers attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) (types.ScalewayResolverResults, error) {
c.Lock.Lock()
defer c.Lock.Unlock()
var res types.ScalewayResolverResults
var exactMatches types.ScalewayResolverResults
if acceptUUID && anonuuid.IsUUID(needle) == nil {
if fields, ok := c.Servers[needle]; ok {
entry, err := types.NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierServer)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*"))
for identifier, fields := range c.Servers {
if fields[CacheTitle] == needle {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierServer)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
exactMatches = append(exactMatches, entry)
}
if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) {
entry, err := types.NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], fields[CacheRegion], types.IdentifierServer)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
res = append(res, entry)
}
}
if len(exactMatches) == 1 {
return exactMatches, nil
}
return removeDuplicatesResults(res), nil
}
// removeDuplicatesResults transforms an array into a unique array
func removeDuplicatesResults(elements types.ScalewayResolverResults) types.ScalewayResolverResults {
encountered := map[string]types.ScalewayResolverResult{}
// Create a map of all unique elements.
for v := range elements {
encountered[elements[v].Identifier] = elements[v]
}
// Place all keys from the map into a slice.
results := types.ScalewayResolverResults{}
for _, result := range encountered {
results = append(results, result)
}
return results
}
// LookUpIdentifiers attempts to return identifiers matching a pattern
func (c *ScalewayCache) LookUpIdentifiers(needle string) (types.ScalewayResolverResults, error) {
results := types.ScalewayResolverResults{}
identifierType, needle := types.ParseNeedle(needle)
if identifierType&(types.IdentifierUnknown|types.IdentifierServer) > 0 {
servers, err := c.LookUpServers(needle, false)
if err != nil {
return types.ScalewayResolverResults{}, err
}
for _, result := range servers {
entry, err := types.NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, result.Region, types.IdentifierServer)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
results = append(results, entry)
}
}
if identifierType&(types.IdentifierUnknown|types.IdentifierImage) > 0 {
images, err := c.LookUpImages(needle, false)
if err != nil {
return types.ScalewayResolverResults{}, err
}
for _, result := range images {
entry, err := types.NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, result.Region, types.IdentifierImage)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
results = append(results, entry)
}
}
if identifierType&(types.IdentifierUnknown|types.IdentifierSnapshot) > 0 {
snapshots, err := c.LookUpSnapshots(needle, false)
if err != nil {
return types.ScalewayResolverResults{}, err
}
for _, result := range snapshots {
entry, err := types.NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, result.Region, types.IdentifierSnapshot)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
results = append(results, entry)
}
}
if identifierType&(types.IdentifierUnknown|types.IdentifierVolume) > 0 {
volumes, err := c.LookUpVolumes(needle, false)
if err != nil {
return types.ScalewayResolverResults{}, err
}
for _, result := range volumes {
entry, err := types.NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, result.Region, types.IdentifierVolume)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
results = append(results, entry)
}
}
if identifierType&(types.IdentifierUnknown|types.IdentifierBootscript) > 0 {
bootscripts, err := c.LookUpBootscripts(needle, false)
if err != nil {
return types.ScalewayResolverResults{}, err
}
for _, result := range bootscripts {
entry, err := types.NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, result.Region, types.IdentifierBootscript)
if err != nil {
return types.ScalewayResolverResults{}, err
}
ComputeRankMatch(&entry, needle)
results = append(results, entry)
}
}
return results, nil
}
// InsertServer registers a server in the cache
func (c *ScalewayCache) InsertServer(identifier, region, arch, owner, name string) {
c.Lock.Lock()
defer c.Lock.Unlock()
fields, exists := c.Servers[identifier]
if !exists || fields[CacheTitle] != name {
c.Servers[identifier] = [CacheMaxfield]string{region, arch, owner, name}
c.Modified = true
}
}
// RemoveServer removes a server from the cache
func (c *ScalewayCache) RemoveServer(identifier string) {
c.Lock.Lock()
defer c.Lock.Unlock()
delete(c.Servers, identifier)
c.Modified = true
}
// ClearServers removes all servers from the cache
func (c *ScalewayCache) ClearServers() {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Servers = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// InsertImage registers an image in the cache
func (c *ScalewayCache) InsertImage(identifier, region, arch, owner, name, marketPlaceUUID string) {
c.Lock.Lock()
defer c.Lock.Unlock()
fields, exists := c.Images[identifier]
if !exists || fields[CacheTitle] != name {
c.Images[identifier] = [CacheMaxfield]string{region, arch, owner, name, marketPlaceUUID}
c.Modified = true
}
}
// RemoveImage removes a server from the cache
func (c *ScalewayCache) RemoveImage(identifier string) {
c.Lock.Lock()
defer c.Lock.Unlock()
delete(c.Images, identifier)
c.Modified = true
}
// ClearImages removes all images from the cache
func (c *ScalewayCache) ClearImages() {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Images = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// InsertSnapshot registers an snapshot in the cache
func (c *ScalewayCache) InsertSnapshot(identifier, region, arch, owner, name string) {
c.Lock.Lock()
defer c.Lock.Unlock()
fields, exists := c.Snapshots[identifier]
if !exists || fields[CacheTitle] != name {
c.Snapshots[identifier] = [CacheMaxfield]string{region, arch, owner, name}
c.Modified = true
}
}
// RemoveSnapshot removes a server from the cache
func (c *ScalewayCache) RemoveSnapshot(identifier string) {
c.Lock.Lock()
defer c.Lock.Unlock()
delete(c.Snapshots, identifier)
c.Modified = true
}
// ClearSnapshots removes all snapshots from the cache
func (c *ScalewayCache) ClearSnapshots() {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Snapshots = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// InsertVolume registers an volume in the cache
func (c *ScalewayCache) InsertVolume(identifier, region, arch, owner, name string) {
c.Lock.Lock()
defer c.Lock.Unlock()
fields, exists := c.Volumes[identifier]
if !exists || fields[CacheTitle] != name {
c.Volumes[identifier] = [CacheMaxfield]string{region, arch, owner, name}
c.Modified = true
}
}
// RemoveVolume removes a server from the cache
func (c *ScalewayCache) RemoveVolume(identifier string) {
c.Lock.Lock()
defer c.Lock.Unlock()
delete(c.Volumes, identifier)
c.Modified = true
}
// ClearVolumes removes all volumes from the cache
func (c *ScalewayCache) ClearVolumes() {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Volumes = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// InsertBootscript registers an bootscript in the cache
func (c *ScalewayCache) InsertBootscript(identifier, region, arch, owner, name string) {
c.Lock.Lock()
defer c.Lock.Unlock()
fields, exists := c.Bootscripts[identifier]
if !exists || fields[CacheTitle] != name {
c.Bootscripts[identifier] = [CacheMaxfield]string{region, arch, owner, name}
c.Modified = true
}
}
// RemoveBootscript removes a bootscript from the cache
func (c *ScalewayCache) RemoveBootscript(identifier string) {
c.Lock.Lock()
defer c.Lock.Unlock()
delete(c.Bootscripts, identifier)
c.Modified = true
}
// ClearBootscripts removes all bootscripts from the cache
func (c *ScalewayCache) ClearBootscripts() {
c.Lock.Lock()
defer c.Lock.Unlock()
c.Bootscripts = make(map[string][CacheMaxfield]string)
c.Modified = true
}
// GetNbServers returns the number of servers in the cache
func (c *ScalewayCache) GetNbServers() int {
c.Lock.Lock()
defer c.Lock.Unlock()
return len(c.Servers)
}
// GetNbImages returns the number of images in the cache
func (c *ScalewayCache) GetNbImages() int {
c.Lock.Lock()
defer c.Lock.Unlock()
return len(c.Images)
}
// GetNbSnapshots returns the number of snapshots in the cache
func (c *ScalewayCache) GetNbSnapshots() int {
c.Lock.Lock()
defer c.Lock.Unlock()
return len(c.Snapshots)
}
// GetNbVolumes returns the number of volumes in the cache
func (c *ScalewayCache) GetNbVolumes() int {
c.Lock.Lock()
defer c.Lock.Unlock()
return len(c.Volumes)
}
// GetNbBootscripts returns the number of bootscripts in the cache
func (c *ScalewayCache) GetNbBootscripts() int {
c.Lock.Lock()
defer c.Lock.Unlock()
return len(c.Bootscripts)
}

View File

@ -1,77 +0,0 @@
// Copyright (C) 2018 Scaleway. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.md file.
package logger
import (
"fmt"
"log"
"net/http"
"os"
)
// Logger handles logging concerns for the Scaleway API SDK
type Logger interface {
LogHTTP(*http.Request)
Fatalf(format string, v ...interface{})
Debugf(format string, v ...interface{})
Infof(format string, v ...interface{})
Warnf(format string, v ...interface{})
}
// NewDefaultLogger returns a logger which is configured for stdout
func NewDefaultLogger() Logger {
return &defaultLogger{
Logger: log.New(os.Stdout, "", log.LstdFlags),
}
}
type defaultLogger struct {
*log.Logger
}
func (l *defaultLogger) LogHTTP(r *http.Request) {
l.Printf("%s %s\n", r.Method, r.URL.RawPath)
}
func (l *defaultLogger) Fatalf(format string, v ...interface{}) {
l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v))
os.Exit(1)
}
func (l *defaultLogger) Debugf(format string, v ...interface{}) {
l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v))
}
func (l *defaultLogger) Infof(format string, v ...interface{}) {
l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v))
}
func (l *defaultLogger) Warnf(format string, v ...interface{}) {
l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v))
}
type disableLogger struct {
}
// NewDisableLogger returns a logger which is configured to do nothing
func NewDisableLogger() Logger {
return &disableLogger{}
}
func (d *disableLogger) LogHTTP(r *http.Request) {
}
func (d *disableLogger) Fatalf(format string, v ...interface{}) {
panic(fmt.Sprintf(format, v))
}
func (d *disableLogger) Debugf(format string, v ...interface{}) {
}
func (d *disableLogger) Infof(format string, v ...interface{}) {
}
func (d *disableLogger) Warnf(format string, v ...interface{}) {
}

View File

@ -1,983 +0,0 @@
// Copyright (C) 2018 Scaleway. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.md file.
package types
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"regexp"
"strings"
"time"
"github.com/moul/anonuuid"
)
// ParseNeedle parses a user needle and try to extract a forced object type
// i.e:
// - server:blah-blah -> kind=server, needle=blah-blah
// - blah-blah -> kind="", needle=blah-blah
// - not-existing-type:blah-blah
func ParseNeedle(input string) (identifierType int, needle string) {
parts := strings.Split(input, ":")
if len(parts) == 2 {
switch parts[0] {
case "server":
return IdentifierServer, parts[1]
case "image":
return IdentifierImage, parts[1]
case "snapshot":
return IdentifierSnapshot, parts[1]
case "bootscript":
return IdentifierBootscript, parts[1]
case "volume":
return IdentifierVolume, parts[1]
}
}
return IdentifierUnknown, input
}
const (
// IdentifierUnknown is used when we don't know explicitly the type key of the object (used for nil comparison)
IdentifierUnknown = 1 << iota
// IdentifierServer is the type key of cached server objects
IdentifierServer
// IdentifierImage is the type key of cached image objects
IdentifierImage
// IdentifierSnapshot is the type key of cached snapshot objects
IdentifierSnapshot
// IdentifierBootscript is the type key of cached bootscript objects
IdentifierBootscript
// IdentifierVolume is the type key of cached volume objects
IdentifierVolume
)
// ScalewayResolverResult is a structure containing human-readable information
// about resolver results. This structure is used to display the user choices.
type ScalewayResolverResult struct {
Identifier string
Type int
Name string
Arch string
Needle string
RankMatch int
Region string
}
// ScalewayResolverResults is a list of `ScalewayResolverResult`
type ScalewayResolverResults []ScalewayResolverResult
// NewScalewayResolverResult returns a new ScalewayResolverResult
func NewScalewayResolverResult(Identifier, Name, Arch, Region string, Type int) (ScalewayResolverResult, error) {
if err := anonuuid.IsUUID(Identifier); err != nil {
return ScalewayResolverResult{}, err
}
return ScalewayResolverResult{
Identifier: Identifier,
Type: Type,
Name: Name,
Arch: Arch,
Region: Region,
}, nil
}
func (s ScalewayResolverResults) Len() int {
return len(s)
}
func (s ScalewayResolverResults) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ScalewayResolverResults) Less(i, j int) bool {
return s[i].RankMatch < s[j].RankMatch
}
// TruncIdentifier returns first 8 characters of an Identifier (UUID)
func (s *ScalewayResolverResult) TruncIdentifier() string {
return s.Identifier[:8]
}
func identifierTypeName(kind int) string {
switch kind {
case IdentifierServer:
return "Server"
case IdentifierImage:
return "Image"
case IdentifierSnapshot:
return "Snapshot"
case IdentifierVolume:
return "Volume"
case IdentifierBootscript:
return "Bootscript"
}
return ""
}
// CodeName returns a full resource name with typed prefix
func (s *ScalewayResolverResult) CodeName() string {
name := strings.ToLower(s.Name)
name = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(name, "-")
name = regexp.MustCompile(`--+`).ReplaceAllString(name, "-")
name = strings.Trim(name, "-")
return fmt.Sprintf("%s:%s", strings.ToLower(identifierTypeName(s.Type)), name)
}
// FilterByArch deletes the elements which not match with arch
func (s *ScalewayResolverResults) FilterByArch(arch string) {
REDO:
for i := range *s {
if (*s)[i].Arch != arch {
(*s)[i] = (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
goto REDO
}
}
}
// ScalewayAPIError represents a Scaleway API Error
type ScalewayAPIError struct {
// Message is a human-friendly error message
APIMessage string `json:"message,omitempty"`
// Type is a string code that defines the kind of error
Type string `json:"type,omitempty"`
// Fields contains detail about validation error
Fields map[string][]string `json:"fields,omitempty"`
// StatusCode is the HTTP status code received
StatusCode int `json:"-"`
// Message
Message string `json:"-"`
}
// Error returns a string representing the error
func (e ScalewayAPIError) Error() string {
var b bytes.Buffer
fmt.Fprintf(&b, "StatusCode: %v, ", e.StatusCode)
fmt.Fprintf(&b, "Type: %v, ", e.Type)
fmt.Fprintf(&b, "APIMessage: \x1b[31m%v\x1b[0m", e.APIMessage)
if len(e.Fields) > 0 {
fmt.Fprintf(&b, ", Details: %v", e.Fields)
}
return b.String()
}
// ScalewayIPAddress represents a Scaleway IP address
type ScalewayIPAddress struct {
// Identifier is a unique identifier for the IP address
Identifier string `json:"id,omitempty"`
// IP is an IPv4 address
IP string `json:"address,omitempty"`
// Dynamic is a flag that defines an IP that change on each reboot
Dynamic *bool `json:"dynamic,omitempty"`
}
// ScalewayVolume represents a Scaleway Volume
type ScalewayVolume struct {
// Identifier is a unique identifier for the volume
Identifier string `json:"id,omitempty"`
// Size is the allocated size of the volume
Size uint64 `json:"size,omitempty"`
// CreationDate is the creation date of the volume
CreationDate string `json:"creation_date,omitempty"`
// ModificationDate is the date of the last modification of the volume
ModificationDate string `json:"modification_date,omitempty"`
// Organization is the organization owning the volume
Organization string `json:"organization,omitempty"`
// Name is the name of the volume
Name string `json:"name,omitempty"`
// Server is the server using this image
Server *struct {
Identifier string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
} `json:"server,omitempty"`
// VolumeType is a Scaleway identifier for the kind of volume (default: l_ssd)
VolumeType string `json:"volume_type,omitempty"`
// ExportURI represents the url used by initrd/scripts to attach the volume
ExportURI string `json:"export_uri,omitempty"`
}
// ScalewayOneVolume represents the response of a GET /volumes/UUID API call
type ScalewayOneVolume struct {
Volume ScalewayVolume `json:"volume,omitempty"`
}
// ScalewayVolumes represents a group of Scaleway volumes
type ScalewayVolumes struct {
// Volumes holds scaleway volumes of the response
Volumes []ScalewayVolume `json:"volumes,omitempty"`
}
// ScalewayVolumeDefinition represents a Scaleway volume definition
type ScalewayVolumeDefinition struct {
// Name is the user-defined name of the volume
Name string `json:"name"`
// Image is the image used by the volume
Size uint64 `json:"size"`
// Bootscript is the bootscript used by the volume
Type string `json:"volume_type"`
// Organization is the owner of the volume
Organization string `json:"organization"`
}
// ScalewayVolumePutDefinition represents a Scaleway volume with nullable fields (for PUT)
type ScalewayVolumePutDefinition struct {
Identifier *string `json:"id,omitempty"`
Size *uint64 `json:"size,omitempty"`
CreationDate *string `json:"creation_date,omitempty"`
ModificationDate *string `json:"modification_date,omitempty"`
Organization *string `json:"organization,omitempty"`
Name *string `json:"name,omitempty"`
Server struct {
Identifier *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
} `json:"server,omitempty"`
VolumeType *string `json:"volume_type,omitempty"`
ExportURI *string `json:"export_uri,omitempty"`
}
// ScalewayImage represents a Scaleway Image
type ScalewayImage struct {
// Identifier is a unique identifier for the image
Identifier string `json:"id,omitempty"`
// Name is a user-defined name for the image
Name string `json:"name,omitempty"`
// CreationDate is the creation date of the image
CreationDate string `json:"creation_date,omitempty"`
// ModificationDate is the date of the last modification of the image
ModificationDate string `json:"modification_date,omitempty"`
// RootVolume is the root volume bound to the image
RootVolume ScalewayVolume `json:"root_volume,omitempty"`
// Public is true for public images and false for user images
Public bool `json:"public,omitempty"`
// Bootscript is the bootscript bound to the image
DefaultBootscript *ScalewayBootscript `json:"default_bootscript,omitempty"`
// Organization is the owner of the image
Organization string `json:"organization,omitempty"`
// Arch is the architecture target of the image
Arch string `json:"arch,omitempty"`
// FIXME: extra_volumes
}
// ScalewayImageIdentifier represents a Scaleway Image Identifier
type ScalewayImageIdentifier struct {
Identifier string
Arch string
Region string
Owner string
}
// ScalewayOneImage represents the response of a GET /images/UUID API call
type ScalewayOneImage struct {
Image ScalewayImage `json:"image,omitempty"`
}
// ScalewayImages represents a group of Scaleway images
type ScalewayImages struct {
// Images holds scaleway images of the response
Images []ScalewayImage `json:"images,omitempty"`
}
// ProductNetworkInterface gives interval and external allowed bandwidth
type ProductNetworkInterface struct {
InternalBandwidth uint64 `json:"internal_bandwidth,omitempty"`
InternetBandwidth uint64 `json:"internet_bandwidth,omitempty"`
}
// ProductNetwork lists all the network interfaces
type ProductNetwork struct {
Interfaces []ProductNetworkInterface `json:"interfaces,omitempty"`
TotalInternalBandwidth uint64 `json:"sum_internal_bandwidth,omitempty"`
TotalInternetBandwidth uint64 `json:"sum_internet_bandwidth,omitempty"`
IPv6_Support bool `json:"ipv6_support,omitempty"`
}
// ProductVolumeConstraint contains any volume constraint that the offer has
type ProductVolumeConstraint struct {
MinSize uint64 `json:"min_size,omitempty"`
MaxSize uint64 `json:"max_size,omitempty"`
}
// ProductServerOffer represents a specific offer
type ProductServer struct {
Arch string `json:"arch,omitempty"`
Ncpus uint64 `json:"ncpus,omitempty"`
Ram uint64 `json:"ram,omitempty"`
Baremetal bool `json:"baremetal,omitempty"`
VolumesConstraint ProductVolumeConstraint `json:"volumes_constraint,omitempty"`
AltNames []string `json:"alt_names,omitempty"`
Network ProductNetwork `json:"network,omitempty"`
}
// Products holds a map of all Scaleway servers
type ScalewayProductsServers struct {
Servers map[string]ProductServer `json:"servers"`
}
// ScalewaySnapshot represents a Scaleway Snapshot
type ScalewaySnapshot struct {
// Identifier is a unique identifier for the snapshot
Identifier string `json:"id,omitempty"`
// Name is a user-defined name for the snapshot
Name string `json:"name,omitempty"`
// CreationDate is the creation date of the snapshot
CreationDate string `json:"creation_date,omitempty"`
// ModificationDate is the date of the last modification of the snapshot
ModificationDate string `json:"modification_date,omitempty"`
// Size is the allocated size of the volume
Size uint64 `json:"size,omitempty"`
// Organization is the owner of the snapshot
Organization string `json:"organization"`
// State is the current state of the snapshot
State string `json:"state"`
// VolumeType is the kind of volume behind the snapshot
VolumeType string `json:"volume_type"`
// BaseVolume is the volume from which the snapshot inherits
BaseVolume ScalewayVolume `json:"base_volume,omitempty"`
}
// ScalewayOneSnapshot represents the response of a GET /snapshots/UUID API call
type ScalewayOneSnapshot struct {
Snapshot ScalewaySnapshot `json:"snapshot,omitempty"`
}
// ScalewaySnapshots represents a group of Scaleway snapshots
type ScalewaySnapshots struct {
// Snapshots holds scaleway snapshots of the response
Snapshots []ScalewaySnapshot `json:"snapshots,omitempty"`
}
// ScalewayBootscript represents a Scaleway Bootscript
type ScalewayBootscript struct {
Bootcmdargs string `json:"bootcmdargs,omitempty"`
Dtb string `json:"dtb,omitempty"`
Initrd string `json:"initrd,omitempty"`
Kernel string `json:"kernel,omitempty"`
// Arch is the architecture target of the bootscript
Arch string `json:"architecture,omitempty"`
// Identifier is a unique identifier for the bootscript
Identifier string `json:"id,omitempty"`
// Organization is the owner of the bootscript
Organization string `json:"organization,omitempty"`
// Name is a user-defined name for the bootscript
Title string `json:"title,omitempty"`
// Public is true for public bootscripts and false for user bootscripts
Public bool `json:"public,omitempty"`
Default bool `json:"default,omitempty"`
}
// ScalewayOneBootscript represents the response of a GET /bootscripts/UUID API call
type ScalewayOneBootscript struct {
Bootscript ScalewayBootscript `json:"bootscript,omitempty"`
}
// ScalewayBootscripts represents a group of Scaleway bootscripts
type ScalewayBootscripts struct {
// Bootscripts holds Scaleway bootscripts of the response
Bootscripts []ScalewayBootscript `json:"bootscripts,omitempty"`
}
// ScalewayTask represents a Scaleway Task
type ScalewayTask struct {
// Identifier is a unique identifier for the task
Identifier string `json:"id,omitempty"`
// StartDate is the start date of the task
StartDate string `json:"started_at,omitempty"`
// TerminationDate is the termination date of the task
TerminationDate string `json:"terminated_at,omitempty"`
HrefFrom string `json:"href_from,omitempty"`
Description string `json:"description,omitempty"`
Status string `json:"status,omitempty"`
Progress int `json:"progress,omitempty"`
}
// ScalewayOneTask represents the response of a GET /tasks/UUID API call
type ScalewayOneTask struct {
Task ScalewayTask `json:"task,omitempty"`
}
// ScalewayTasks represents a group of Scaleway tasks
type ScalewayTasks struct {
// Tasks holds scaleway tasks of the response
Tasks []ScalewayTask `json:"tasks,omitempty"`
}
// ScalewaySecurityGroupRule definition
type ScalewaySecurityGroupRule struct {
Direction string `json:"direction"`
Protocol string `json:"protocol"`
IPRange string `json:"ip_range"`
DestPortFrom int `json:"dest_port_from,omitempty"`
Action string `json:"action"`
Position int `json:"position"`
DestPortTo string `json:"dest_port_to"`
Editable bool `json:"editable"`
ID string `json:"id"`
}
// ScalewayGetSecurityGroupRules represents the response of a GET /security_group/{groupID}/rules
type ScalewayGetSecurityGroupRules struct {
Rules []ScalewaySecurityGroupRule `json:"rules"`
}
// ScalewayGetSecurityGroupRule represents the response of a GET /security_group/{groupID}/rules/{ruleID}
type ScalewayGetSecurityGroupRule struct {
Rules ScalewaySecurityGroupRule `json:"rule"`
}
// ScalewayNewSecurityGroupRule definition POST/PUT request /security_group/{groupID}
type ScalewayNewSecurityGroupRule struct {
Action string `json:"action"`
Direction string `json:"direction"`
IPRange string `json:"ip_range"`
Protocol string `json:"protocol"`
DestPortFrom int `json:"dest_port_from,omitempty"`
}
// ScalewaySecurityGroups definition
type ScalewaySecurityGroups struct {
Description string `json:"description"`
ID string `json:"id"`
Organization string `json:"organization"`
Name string `json:"name"`
Servers []ScalewaySecurityGroup `json:"servers"`
EnableDefaultSecurity bool `json:"enable_default_security"`
OrganizationDefault bool `json:"organization_default"`
}
// ScalewayGetSecurityGroups represents the response of a GET /security_groups/
type ScalewayGetSecurityGroups struct {
SecurityGroups []ScalewaySecurityGroups `json:"security_groups"`
}
// ScalewayGetSecurityGroup represents the response of a GET /security_groups/{groupID}
type ScalewayGetSecurityGroup struct {
SecurityGroups ScalewaySecurityGroups `json:"security_group"`
}
// ScalewayIPDefinition represents the IP's fields
type ScalewayIPDefinition struct {
Organization string `json:"organization"`
Reverse *string `json:"reverse"`
ID string `json:"id"`
Server *struct {
Identifier string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
} `json:"server"`
Address string `json:"address"`
}
// ScalewayGetIPS represents the response of a GET /ips/
type ScalewayGetIPS struct {
IPS []ScalewayIPDefinition `json:"ips"`
}
// ScalewayGetIP represents the response of a GET /ips/{id_ip}
type ScalewayGetIP struct {
IP ScalewayIPDefinition `json:"ip"`
}
// ScalewaySecurityGroup represents a Scaleway security group
type ScalewaySecurityGroup struct {
// Identifier is a unique identifier for the security group
Identifier string `json:"id,omitempty"`
// Name is the user-defined name of the security group
Name string `json:"name,omitempty"`
}
// ScalewayNewSecurityGroup definition POST request /security_groups
type ScalewayNewSecurityGroup struct {
Organization string `json:"organization"`
Name string `json:"name"`
Description string `json:"description"`
}
// ScalewayUpdateSecurityGroup definition PUT request /security_groups
type ScalewayUpdateSecurityGroup struct {
Organization string `json:"organization"`
Name string `json:"name"`
Description string `json:"description"`
OrganizationDefault bool `json:"organization_default"`
}
// ScalewayServer represents a Scaleway server
type ScalewayServer struct {
// Arch is the architecture target of the server
Arch string `json:"arch,omitempty"`
// Identifier is a unique identifier for the server
Identifier string `json:"id,omitempty"`
// Name is the user-defined name of the server
Name string `json:"name,omitempty"`
// CreationDate is the creation date of the server
CreationDate string `json:"creation_date,omitempty"`
// ModificationDate is the date of the last modification of the server
ModificationDate string `json:"modification_date,omitempty"`
// Image is the image used by the server
Image ScalewayImage `json:"image,omitempty"`
// DynamicIPRequired is a flag that defines a server with a dynamic ip address attached
DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"`
// PublicIP is the public IP address bound to the server
PublicAddress ScalewayIPAddress `json:"public_ip,omitempty"`
// State is the current status of the server
State string `json:"state,omitempty"`
// StateDetail is the detailed status of the server
StateDetail string `json:"state_detail,omitempty"`
// PrivateIP represents the private IPV4 attached to the server (changes on each boot)
PrivateIP string `json:"private_ip,omitempty"`
// Bootscript is the unique identifier of the selected bootscript
Bootscript *ScalewayBootscript `json:"bootscript,omitempty"`
// Hostname represents the ServerName in a format compatible with unix's hostname
Hostname string `json:"hostname,omitempty"`
// Tags represents user-defined tags
Tags []string `json:"tags,omitempty"`
// Volumes are the attached volumes
Volumes map[string]ScalewayVolume `json:"volumes,omitempty"`
// SecurityGroup is the selected security group object
SecurityGroup ScalewaySecurityGroup `json:"security_group,omitempty"`
// Organization is the owner of the server
Organization string `json:"organization,omitempty"`
// CommercialType is the commercial type of the server (i.e: C1, C2[SML], VC1S)
CommercialType string `json:"commercial_type,omitempty"`
// Location of the server
Location struct {
Platform string `json:"platform_id,omitempty"`
Chassis string `json:"chassis_id,omitempty"`
Cluster string `json:"cluster_id,omitempty"`
Hypervisor string `json:"hypervisor_id,omitempty"`
Blade string `json:"blade_id,omitempty"`
Node string `json:"node_id,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
} `json:"location,omitempty"`
IPV6 *ScalewayIPV6Definition `json:"ipv6,omitempty"`
EnableIPV6 bool `json:"enable_ipv6,omitempty"`
// This fields are not returned by the API, we generate it
DNSPublic string `json:"dns_public,omitempty"`
DNSPrivate string `json:"dns_private,omitempty"`
}
// ScalewayIPV6Definition represents a Scaleway ipv6
type ScalewayIPV6Definition struct {
Netmask string `json:"netmask"`
Gateway string `json:"gateway"`
Address string `json:"address"`
}
// ScalewayServerPatchDefinition represents a Scaleway server with nullable fields (for PATCH)
type ScalewayServerPatchDefinition struct {
Arch *string `json:"arch,omitempty"`
Name *string `json:"name,omitempty"`
CreationDate *string `json:"creation_date,omitempty"`
ModificationDate *string `json:"modification_date,omitempty"`
Image *ScalewayImage `json:"image,omitempty"`
DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"`
PublicAddress *ScalewayIPAddress `json:"public_ip,omitempty"`
State *string `json:"state,omitempty"`
StateDetail *string `json:"state_detail,omitempty"`
PrivateIP *string `json:"private_ip,omitempty"`
Bootscript *string `json:"bootscript,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Volumes *map[string]ScalewayVolume `json:"volumes,omitempty"`
SecurityGroup *ScalewaySecurityGroup `json:"security_group,omitempty"`
Organization *string `json:"organization,omitempty"`
Tags *[]string `json:"tags,omitempty"`
IPV6 *ScalewayIPV6Definition `json:"ipv6,omitempty"`
EnableIPV6 *bool `json:"enable_ipv6,omitempty"`
}
// ScalewayServerDefinition represents a Scaleway server with image definition
type ScalewayServerDefinition struct {
// Name is the user-defined name of the server
Name string `json:"name"`
// Image is the image used by the server
Image *string `json:"image,omitempty"`
// Volumes are the attached volumes
Volumes map[string]string `json:"volumes,omitempty"`
// DynamicIPRequired is a flag that defines a server with a dynamic ip address attached
DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"`
// BootType defines the type of boot
BootType string `json:"boot_type,omitempty"`
// Bootscript is the bootscript used by the server
Bootscript *string `json:"bootscript"`
// Tags are the metadata tags attached to the server
Tags []string `json:"tags,omitempty"`
// Organization is the owner of the server
Organization string `json:"organization"`
// CommercialType is the commercial type of the server (i.e: C1, C2[SML], VC1S)
CommercialType string `json:"commercial_type"`
PublicIP string `json:"public_ip,omitempty"`
EnableIPV6 bool `json:"enable_ipv6,omitempty"`
SecurityGroup string `json:"security_group,omitempty"`
}
// ScalewayOneServer represents the response of a GET /servers/UUID API call
type ScalewayOneServer struct {
Server ScalewayServer `json:"server,omitempty"`
}
// ScalewayServers represents a group of Scaleway servers
type ScalewayServers struct {
// Servers holds scaleway servers of the response
Servers []ScalewayServer `json:"servers,omitempty"`
}
// ScalewayServerAction represents an action to perform on a Scaleway server
type ScalewayServerAction struct {
// Action is the name of the action to trigger
Action string `json:"action,omitempty"`
}
// ScalewaySnapshotDefinition represents a Scaleway snapshot definition
type ScalewaySnapshotDefinition struct {
VolumeIDentifier string `json:"volume_id"`
Name string `json:"name,omitempty"`
Organization string `json:"organization"`
}
// ScalewayImageDefinition represents a Scaleway image definition
type ScalewayImageDefinition struct {
SnapshotIDentifier string `json:"root_volume"`
Name string `json:"name,omitempty"`
Organization string `json:"organization"`
Arch string `json:"arch"`
DefaultBootscript *string `json:"default_bootscript,omitempty"`
}
// ScalewayRoleDefinition represents a Scaleway Token UserId Role
type ScalewayRoleDefinition struct {
Organization ScalewayOrganizationDefinition `json:"organization,omitempty"`
Role string `json:"role,omitempty"`
}
// ScalewayTokenDefinition represents a Scaleway Token
type ScalewayTokenDefinition struct {
UserID string `json:"user_id"`
Description string `json:"description,omitempty"`
Roles ScalewayRoleDefinition `json:"roles"`
Expires string `json:"expires"`
InheritsUsersPerms bool `json:"inherits_user_perms"`
ID string `json:"id"`
}
// ScalewayTokensDefinition represents a Scaleway Tokens
type ScalewayTokensDefinition struct {
Token ScalewayTokenDefinition `json:"token"`
}
// ScalewayGetTokens represents a list of Scaleway Tokens
type ScalewayGetTokens struct {
Tokens []ScalewayTokenDefinition `json:"tokens"`
}
// ScalewayContainerData represents a Scaleway container data (S3)
type ScalewayContainerData struct {
LastModified string `json:"last_modified"`
Name string `json:"name"`
Size string `json:"size"`
}
// ScalewayGetContainerDatas represents a list of Scaleway containers data (S3)
type ScalewayGetContainerDatas struct {
Container []ScalewayContainerData `json:"container"`
}
// ScalewayContainer represents a Scaleway container (S3)
type ScalewayContainer struct {
ScalewayOrganizationDefinition `json:"organization"`
Name string `json:"name"`
Size string `json:"size"`
}
// ScalewayGetContainers represents a list of Scaleway containers (S3)
type ScalewayGetContainers struct {
Containers []ScalewayContainer `json:"containers"`
}
// ScalewayConnectResponse represents the answer from POST /tokens
type ScalewayConnectResponse struct {
Token ScalewayTokenDefinition `json:"token"`
}
// ScalewayConnect represents the data to connect
type ScalewayConnect struct {
Email string `json:"email"`
Password string `json:"password"`
Description string `json:"description"`
Expires bool `json:"expires"`
}
// ScalewayConnectInterface is the interface implemented by ScalewayConnect,
// ScalewayConnectByOTP and ScalewayConnectByBackupCode
type ScalewayConnectInterface interface {
GetPassword() string
}
func (s *ScalewayConnect) GetPassword() string {
return s.Password
}
type ScalewayConnectByOTP struct {
ScalewayConnect
TwoFAToken string `json:"2FA_token"`
}
type ScalewayConnectByBackupCode struct {
ScalewayConnect
TwoFABackupCode string `json:"2FA_backup_code"`
}
// ScalewayOrganizationDefinition represents a Scaleway Organization
type ScalewayOrganizationDefinition struct {
ID string `json:"id"`
Name string `json:"name"`
Users []ScalewayUserDefinition `json:"users"`
}
// ScalewayOrganizationsDefinition represents a Scaleway Organizations
type ScalewayOrganizationsDefinition struct {
Organizations []ScalewayOrganizationDefinition `json:"organizations"`
}
// ScalewayUserDefinition represents a Scaleway User
type ScalewayUserDefinition struct {
Email string `json:"email"`
Firstname string `json:"firstname"`
Fullname string `json:"fullname"`
ID string `json:"id"`
Lastname string `json:"lastname"`
Organizations []ScalewayOrganizationDefinition `json:"organizations"`
Roles []ScalewayRoleDefinition `json:"roles"`
SSHPublicKeys []ScalewayKeyDefinition `json:"ssh_public_keys"`
}
// ScalewayUsersDefinition represents the response of a GET /user
type ScalewayUsersDefinition struct {
User ScalewayUserDefinition `json:"user"`
}
// ScalewayKeyDefinition represents a key
type ScalewayKeyDefinition struct {
Key string `json:"key"`
Fingerprint string `json:"fingerprint,omitempty"`
}
// ScalewayUserPatchSSHKeyDefinition represents a User Patch
type ScalewayUserPatchSSHKeyDefinition struct {
SSHPublicKeys []ScalewayKeyDefinition `json:"ssh_public_keys"`
}
// ScalewayDashboardResp represents a dashboard received from the API
type ScalewayDashboardResp struct {
Dashboard ScalewayDashboard
}
// ScalewayDashboard represents a dashboard
type ScalewayDashboard struct {
VolumesCount int `json:"volumes_count"`
RunningServersCount int `json:"running_servers_count"`
ImagesCount int `json:"images_count"`
SnapshotsCount int `json:"snapshots_count"`
ServersCount int `json:"servers_count"`
IPsCount int `json:"ips_count"`
}
// ScalewayPermissions represents the response of GET /permissions
type ScalewayPermissions map[string]ScalewayPermCategory
// ScalewayPermCategory represents ScalewayPermissions's fields
type ScalewayPermCategory map[string][]string
// ScalewayPermissionDefinition represents the permissions
type ScalewayPermissionDefinition struct {
Permissions ScalewayPermissions `json:"permissions"`
}
// ScalewayUserdatas represents the response of a GET /user_data
type ScalewayUserdatas struct {
UserData []string `json:"user_data"`
}
// ScalewayQuota represents a map of quota (name, value)
type ScalewayQuota map[string]int
// ScalewayGetQuotas represents the response of GET /organizations/{orga_id}/quotas
type ScalewayGetQuotas struct {
Quotas ScalewayQuota `json:"quotas"`
}
// ScalewayUserdata represents []byte
type ScalewayUserdata []byte
// FuncMap used for json inspection
var FuncMap = template.FuncMap{
"json": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}
// MarketLocalImageDefinition represents localImage of marketplace version
type MarketLocalImageDefinition struct {
Arch string `json:"arch"`
ID string `json:"id"`
Zone string `json:"zone"`
}
// MarketLocalImages represents an array of local images
type MarketLocalImages struct {
LocalImages []MarketLocalImageDefinition `json:"local_images"`
}
// MarketLocalImage represents local image
type MarketLocalImage struct {
LocalImages MarketLocalImageDefinition `json:"local_image"`
}
// MarketVersionDefinition represents version of marketplace image
type MarketVersionDefinition struct {
CreationDate string `json:"creation_date"`
ID string `json:"id"`
Image struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"image"`
ModificationDate string `json:"modification_date"`
Name string `json:"name"`
MarketLocalImages
}
// MarketVersions represents an array of marketplace image versions
type MarketVersions struct {
Versions []MarketVersionDefinition `json:"versions"`
}
// MarketVersion represents version of marketplace image
type MarketVersion struct {
Version MarketVersionDefinition `json:"version"`
}
// MarketImage represents MarketPlace image
type MarketImage struct {
Categories []string `json:"categories"`
CreationDate string `json:"creation_date"`
CurrentPublicVersion string `json:"current_public_version"`
Description string `json:"description"`
ID string `json:"id"`
Logo string `json:"logo"`
ModificationDate string `json:"modification_date"`
Name string `json:"name"`
Organization struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"organization"`
Public bool `json:"-"`
MarketVersions
}
// MarketImages represents MarketPlace images
type MarketImages struct {
Images []MarketImage `json:"images"`
}
// ScalewaySortServers represents a wrapper to sort by CreationDate the servers
type ScalewaySortServers []ScalewayServer
func (s ScalewaySortServers) Len() int {
return len(s)
}
func (s ScalewaySortServers) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ScalewaySortServers) Less(i, j int) bool {
date1, _ := time.Parse("2006-01-02T15:04:05.000000+00:00", s[i].CreationDate)
date2, _ := time.Parse("2006-01-02T15:04:05.000000+00:00", s[j].CreationDate)
return date2.Before(date1)
}
func (s *ScalewayUserdata) String() string {
return string(*s)
}

View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2019 Scaleway.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,81 @@
<p align="center"><img height="125" src="docs/static_files/scaleway-logo.png" /></p>
<p align="center">
<a href="https://godoc.org/github.com/scaleway/scaleway-sdk-go"><img src="https://godoc.org/github.com/scaleway/scaleway-sdk-go?status.svg" alt="GoDoc"/></a>
<a href="https://circleci.com/gh/scaleway/scaleway-sdk-go"><img src="https://circleci.com/gh/scaleway/scaleway-sdk-go.svg?style=shield" alt="CircleCI" /></a>
<a href="https://goreportcard.com/report/github.com/scaleway/scaleway-sdk-go"><img src="https://goreportcard.com/badge/scaleway/scaleway-sdk-go" alt="GoReportCard" /></a>
</p>
# Scaleway GO SDK
**:warning: This is an early release, keep in mind that the API can break**
Scaleway is a simple way to build, deploy and scale your infrastructure in the cloud.
We help thousands of developers and businesses to run their infrastructures without any issue.
## Documentation
- [Godoc](https://godoc.org/github.com/scaleway/scaleway-sdk-go)
- [Developers website](https://developers.scaleway.com) (API documentation)
## Installation
```bash
go get github.com/scaleway/scaleway-sdk-go
```
## Getting Started
```go
package main
import (
"fmt"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/utils"
)
func main() {
// Create a Scaleway client
client, err := scw.NewClient(
// Get your credentials at https://console.scaleway.com/account/credentials
scw.WithDefaultProjectID("ORGANISATION_ID"),
scw.WithAuth("ACCESS_KEY", "SECRET_KEY"),
)
if err != nil {
panic(err)
}
// Create SDK objects for Scaleway Instance product
instanceApi := instance.NewAPI(client)
// Call the ListServers method on the Instance SDK
response, err := instanceApi.ListServers(&instance.ListServersRequest{
Zone: utils.ZoneFrPar1,
})
if err != nil {
panic(err)
}
// Do something with the response...
for _, server := range response.Servers {
fmt.Println("Server", server.ID, server.Name)
}
}
```
## Development
This repository is at its early stage and is still in active development.
If you are looking for a way to contribute please read [CONTRIBUTING.md](CONTRIBUTING.md).
## Reach us
We love feedback.
Feel free to reach us on [Scaleway Slack community](https://slack.scaleway.com/), we are waiting for you on [#opensource](https://scaleway-community.slack.com/app_redirect?channel=opensource).

View File

@ -0,0 +1,278 @@
package instance
import (
"bytes"
"encoding/json"
"io/ioutil"
"math/rand"
"net"
"net/http"
"strconv"
"time"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
)
var (
metadataURL = "http://169.254.42.42"
metadataRetryBindPort = 200
)
// MetadataAPI metadata API
type MetadataAPI struct {
}
// NewMetadataAPI returns a MetadataAPI object from a Scaleway client.
func NewMetadataAPI() *MetadataAPI {
return &MetadataAPI{}
}
// GetMetadata returns the metadata available from the server
func (*MetadataAPI) GetMetadata() (m *Metadata, err error) {
resp, err := http.Get(metadataURL + "/conf?format=json")
if err != nil {
return nil, errors.Wrap(err, "error getting metadataURL")
}
defer resp.Body.Close()
metadata := &Metadata{}
err = json.NewDecoder(resp.Body).Decode(metadata)
if err != nil {
return nil, errors.Wrap(err, "error decoding metadata")
}
return metadata, nil
}
// Metadata represents the struct return by the metadata API
type Metadata struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Hostname string `json:"hostname,omitempty"`
Organization string `json:"organization,omitempty"`
CommercialType string `json:"commercial_type,omitempty"`
PublicIP struct {
Dynamic bool `json:"dynamic,omitempty"`
ID string `json:"id,omitempty"`
Address string `json:"address,omitempty"`
} `json:"public_ip,omitempty"`
PrivateIP string `json:"private_ip,omitempty"`
IPv6 struct {
Netmask string `json:"netmask,omitempty"`
Gateway string `json:"gateway,omitempty"`
Address string `json:"address,omitempty"`
} `json:"ipv6,omitempty"`
Location struct {
PlatformID string `json:"platform_id,omitempty"`
HypervisorID string `json:"hypervisor_id,omitempty"`
NodeID string `json:"node_id,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
} `json:"location,omitempty"`
Tags []string `json:"tags,omitempty"`
StateDetail string `json:"state_detail,omitempty"`
SSHPublicKeys []struct {
Description string `json:"description,omitempty"`
ModificationDate string `json:"modification_date,omitempty"`
IP string `json:"ip,omitempty"`
Email string `json:"email,omitempty"`
UserAgent struct {
Platform string `json:"platform,omitempty"`
Version string `json:"version,omitempty"`
String string `json:"string,omitempty"`
Browser string `json:"browser,omitempty"`
} `json:"user_agent,omitempty"`
Key string `json:"key,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
ID string `json:"id,omitempty"`
CreationDate string `json:"creation_date,omitempty"`
Port int `json:"port,omitempty"`
} `json:"ssh_public_keys,omitempty"`
Timezone string `json:"timezone,omitempty"`
Bootscript struct {
Kernel string `json:"kernel,omitempty"`
Title string `json:"title,omitempty"`
Default bool `json:"default,omitempty"`
Dtb string `json:"dtb,omitempty"`
Public bool `json:"publc,omitempty"`
Initrd string `json:"initrd,omitempty"`
Bootcmdargs string `json:"bootcmdargs,omitempty"`
Architecture string `json:"architecture,omitempty"`
Organization string `json:"organization,omitempty"`
ID string `json:"id,omitempty"`
} `json:"bootscript,omitempty"`
Volumes map[string]struct {
Name string `json:"name,omitempty"`
ModificationDate string `json:"modification_date,omitempty"`
ExportURI string `json:"export_uri,omitempty"`
VolumeType string `json:"volume_type,omitempty"`
CreationDate string `json:"creation_date,omitempty"`
State string `json:"state,omitempty"`
Organization string `json:"organization,omitempty"`
Server struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
} `json:"server,omitempty"`
ID string `json:"id,omitempty"`
Size int `json:"size,omitempty"`
}
}
// ListUserdata returns the metadata available from the server
func (*MetadataAPI) ListUserdata() (res *Userdata, err error) {
retries := 0
for retries <= metadataRetryBindPort {
port := rand.Intn(1024)
localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
if err != nil {
return nil, errors.Wrap(err, "error resolving tcp address")
}
userdataClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
LocalAddr: localTCPAddr,
DualStack: false,
FallbackDelay: time.Second * -1,
}).DialContext,
},
}
resp, err := userdataClient.Get(metadataURL + "/user_data?format=json")
if err != nil {
retries++ // retry with a different source port
continue
}
defer resp.Body.Close()
userdata := &Userdata{}
err = json.NewDecoder(resp.Body).Decode(userdata)
if err != nil {
return nil, errors.Wrap(err, "error decoding userdata")
}
return userdata, nil
}
return nil, errors.New("too many bind port retries for ListUserdata")
}
// GetUserdata returns the value for the given metadata key
func (*MetadataAPI) GetUserdata(key string) ([]byte, error) {
if key == "" {
return make([]byte, 0), errors.New("key must not be empty in GetUserdata")
}
retries := 0
for retries <= metadataRetryBindPort {
port := rand.Intn(1024)
localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
if err != nil {
return make([]byte, 0), errors.Wrap(err, "error resolving tcp address")
}
userdataClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
LocalAddr: localTCPAddr,
DualStack: false,
FallbackDelay: time.Second * -1,
}).DialContext,
},
}
resp, err := userdataClient.Get(metadataURL + "/user_data/" + key)
if err != nil {
retries++ // retry with a different source port
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return make([]byte, 0), errors.Wrap(err, "error reading userdata body")
}
return body, nil
}
return make([]byte, 0), errors.New("too may bind port retries for GetUserdata")
}
// SetUserdata sets the userdata key with the given value
func (*MetadataAPI) SetUserdata(key string, value []byte) error {
if key == "" {
return errors.New("key must not be empty in SetUserdata")
}
retries := 0
for retries <= metadataRetryBindPort {
port := rand.Intn(1024)
localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
if err != nil {
return errors.Wrap(err, "error resolving tcp address")
}
userdataClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
LocalAddr: localTCPAddr,
DualStack: false,
FallbackDelay: time.Second * -1,
}).DialContext,
},
}
request, err := http.NewRequest("PATCH", metadataURL+"/user_data/"+key, bytes.NewBuffer(value))
if err != nil {
return errors.Wrap(err, "error creating patch userdata request")
}
request.Header.Set("Content-Type", "text/plain")
_, err = userdataClient.Do(request)
if err != nil {
retries++ // retry with a different source port
continue
}
return nil
}
return errors.New("too may bind port retries for SetUserdata")
}
// DeleteUserdata deletes the userdata key and the associated value
func (*MetadataAPI) DeleteUserdata(key string) error {
if key == "" {
return errors.New("key must not be empty in DeleteUserdata")
}
retries := 0
for retries <= metadataRetryBindPort {
port := rand.Intn(1024)
localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
if err != nil {
return errors.Wrap(err, "error resolving tcp address")
}
userdataClient := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
LocalAddr: localTCPAddr,
DualStack: false,
FallbackDelay: time.Second * -1,
}).DialContext,
},
}
request, err := http.NewRequest("DELETE", metadataURL+"/user_data/"+key, bytes.NewBuffer([]byte("")))
if err != nil {
return errors.Wrap(err, "error creating delete userdata request")
}
_, err = userdataClient.Do(request)
if err != nil {
retries++ // retry with a different source port
continue
}
return nil
}
return errors.New("too may bind port retries for DeleteUserdata")
}
// Userdata represents the user data
type Userdata struct {
UserData []string `json:"user_data,omitempty"`
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,255 @@
package instance
import (
"fmt"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// AttachIPRequest contains the parameters to attach an IP to a server
type AttachIPRequest struct {
Zone utils.Zone `json:"-"`
IPID string `json:"-"`
ServerID string `json:"server_id"`
}
// AttachIPResponse contains the updated IP after attaching
type AttachIPResponse struct {
IP *IP
}
// AttachIP attaches an IP to a server.
func (s *API) AttachIP(req *AttachIPRequest, opts ...scw.RequestOption) (*AttachIPResponse, error) {
var ptrServerID = &req.ServerID
ipResponse, err := s.updateIP(&updateIPRequest{
Zone: req.Zone,
IPID: req.IPID,
Server: &ptrServerID,
})
if err != nil {
return nil, err
}
return &AttachIPResponse{IP: ipResponse.IP}, nil
}
// DetachIPRequest contains the parameters to detach an IP from a server
type DetachIPRequest struct {
Zone utils.Zone `json:"-"`
IPID string `json:"-"`
}
// DetachIPResponse contains the updated IP after detaching
type DetachIPResponse struct {
IP *IP
}
// DetachIP detaches an IP from a server.
func (s *API) DetachIP(req *DetachIPRequest, opts ...scw.RequestOption) (*DetachIPResponse, error) {
var ptrServerID *string
ipResponse, err := s.updateIP(&updateIPRequest{
Zone: req.Zone,
IPID: req.IPID,
Server: &ptrServerID,
})
if err != nil {
return nil, err
}
return &DetachIPResponse{IP: ipResponse.IP}, nil
}
// UpdateIPRequest contains the parameters to update an IP
// if Reverse is an empty string, the reverse will be removed
type UpdateIPRequest struct {
Zone utils.Zone `json:"-"`
IPID string `json:"-"`
Reverse *string `json:"reverse"`
}
// UpdateIP updates an IP
func (s *API) UpdateIP(req *UpdateIPRequest, opts ...scw.RequestOption) (*UpdateIPResponse, error) {
var reverse **string
if req.Reverse != nil {
if *req.Reverse == "" {
req.Reverse = nil
}
reverse = &req.Reverse
}
ipResponse, err := s.updateIP(&updateIPRequest{
Zone: req.Zone,
IPID: req.IPID,
Reverse: reverse,
})
if err != nil {
return nil, err
}
return &UpdateIPResponse{IP: ipResponse.IP}, nil
}
// AttachVolumeRequest contains the parameters to attach a volume to a server
type AttachVolumeRequest struct {
Zone utils.Zone `json:"-"`
ServerID string `json:"-"`
VolumeID string `json:"-"`
}
// AttachVolumeResponse contains the updated server after attaching a volume
type AttachVolumeResponse struct {
Server *Server `json:"-"`
}
// volumesToVolumeTemplates converts a map of *Volume to a map of *VolumeTemplate
// so it can be used in a UpdateServer request
func volumesToVolumeTemplates(volumes map[string]*Volume) map[string]*VolumeTemplate {
volumeTemplates := map[string]*VolumeTemplate{}
for key, volume := range volumes {
volumeTemplates[key] = &VolumeTemplate{ID: volume.ID, Name: volume.Name}
}
return volumeTemplates
}
// AttachVolume attaches a volume to a server
func (s *API) AttachVolume(req *AttachVolumeRequest, opts ...scw.RequestOption) (*AttachVolumeResponse, error) {
// get server with volumes
getServerResponse, err := s.GetServer(&GetServerRequest{
Zone: req.Zone,
ServerID: req.ServerID,
})
if err != nil {
return nil, err
}
volumes := getServerResponse.Server.Volumes
newVolumes := volumesToVolumeTemplates(volumes)
// add volume to volumes list
key := fmt.Sprintf("%d", len(volumes))
newVolumes[key] = &VolumeTemplate{
ID: req.VolumeID,
// name is ignored on this PATCH
Name: req.VolumeID,
}
// update server
updateServerResponse, err := s.UpdateServer(&UpdateServerRequest{
Zone: req.Zone,
ServerID: req.ServerID,
Volumes: &newVolumes,
})
if err != nil {
return nil, err
}
return &AttachVolumeResponse{Server: updateServerResponse.Server}, nil
}
// DetachVolumeRequest contains the parameters to detach a volume from a server
type DetachVolumeRequest struct {
Zone utils.Zone `json:"-"`
VolumeID string `json:"-"`
}
// DetachVolumeResponse contains the updated server after detaching a volume
type DetachVolumeResponse struct {
Server *Server `json:"-"`
}
// DetachVolume detaches a volume from a server
func (s *API) DetachVolume(req *DetachVolumeRequest, opts ...scw.RequestOption) (*DetachVolumeResponse, error) {
// get volume
getVolumeResponse, err := s.GetVolume(&GetVolumeRequest{
Zone: req.Zone,
VolumeID: req.VolumeID,
})
if err != nil {
return nil, err
}
if getVolumeResponse.Volume == nil {
return nil, fmt.Errorf("expected volume to have value in response")
}
if getVolumeResponse.Volume.Server == nil {
return nil, fmt.Errorf("server should be attached to a server")
}
serverID := getVolumeResponse.Volume.Server.ID
// get server with volumes
getServerResponse, err := s.GetServer(&GetServerRequest{
Zone: req.Zone,
ServerID: serverID,
})
if err != nil {
return nil, err
}
volumes := getServerResponse.Server.Volumes
// remove volume from volumes list
for key, volume := range volumes {
if volume.ID == req.VolumeID {
delete(volumes, key)
}
}
newVolumes := volumesToVolumeTemplates(volumes)
// update server
updateServerResponse, err := s.UpdateServer(&UpdateServerRequest{
Zone: req.Zone,
ServerID: serverID,
Volumes: &newVolumes,
})
if err != nil {
return nil, err
}
return &DetachVolumeResponse{Server: updateServerResponse.Server}, nil
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListServersResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListBootscriptsResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListIpsResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListSecurityGroupRulesResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListSecurityGroupsResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListServersTypesResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListSnapshotsResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}
// UnsafeSetTotalCount should not be used
// Internal usage only
func (r *ListVolumesResponse) UnsafeSetTotalCount(totalCount int) {
r.TotalCount = uint32(totalCount)
}

View File

@ -0,0 +1,53 @@
package instance
import (
"time"
"github.com/scaleway/scaleway-sdk-go/internal/async"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// WaitForServerRequest is used by WaitForServer method
type WaitForServerRequest struct {
ServerID string
Zone utils.Zone
// Timeout: maximum time to wait before (default: 5 minutes)
Timeout time.Duration
}
// WaitForServer wait for the server to be in a "terminal state" before returning.
// This function can be used to wait for a server to be started for example.
func (s *API) WaitForServer(req *WaitForServerRequest) (*Server, error) {
if req.Timeout == 0 {
req.Timeout = 5 * time.Minute
}
terminalStatus := map[ServerState]struct{}{
ServerStateStopped: {},
ServerStateStoppedInPlace: {},
ServerStateLocked: {},
ServerStateRunning: {},
}
server, err := async.WaitSync(&async.WaitSyncConfig{
Get: func() (interface{}, error, bool) {
res, err := s.GetServer(&GetServerRequest{
ServerID: req.ServerID,
Zone: req.Zone,
})
if err != nil {
return nil, err, false
}
_, isTerminal := terminalStatus[res.Server.State]
return res.Server, err, isTerminal
},
Timeout: req.Timeout,
IntervalStrategy: async.LinearIntervalStrategy(5 * time.Second),
})
return server.(*Server), err
}

View File

@ -0,0 +1,265 @@
// This file was automatically generated. DO NOT EDIT.
// If you have any remark or suggestion do not hesitate to open an issue.
package marketplace
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/internal/marshaler"
"github.com/scaleway/scaleway-sdk-go/internal/parameter"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// always import dependencies
var (
_ fmt.Stringer
_ json.Unmarshaler
_ url.URL
_ net.IP
_ http.Header
_ bytes.Reader
_ time.Time
_ scw.ScalewayRequest
_ marshaler.Duration
_ utils.File
_ = parameter.AddToQuery
)
// API marketplace API
type API struct {
client *scw.Client
}
// NewAPI returns a API object from a Scaleway client.
func NewAPI(client *scw.Client) *API {
return &API{
client: client,
}
}
type GetImageResponse struct {
Image *Image `json:"image,omitempty"`
}
type GetServiceInfoResponse struct {
API string `json:"api,omitempty"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
}
type GetVersionResponse struct {
Version *Version `json:"version,omitempty"`
}
type Image struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Logo string `json:"logo,omitempty"`
Categories []string `json:"categories,omitempty"`
Organization *Organization `json:"organization,omitempty"`
ValidUntil time.Time `json:"valid_until,omitempty"`
CreationDate time.Time `json:"creation_date,omitempty"`
ModificationDate time.Time `json:"modification_date,omitempty"`
Versions []*Version `json:"versions,omitempty"`
CurrentPublicVersion string `json:"current_public_version,omitempty"`
}
type ListImagesResponse struct {
Images []*Image `json:"images,omitempty"`
}
type ListVersionsResponse struct {
Versions []*Version `json:"versions,omitempty"`
}
type LocalImage struct {
ID string `json:"id,omitempty"`
Arch string `json:"arch,omitempty"`
Zone utils.Zone `json:"zone,omitempty"`
CompatibleCommercialTypes []string `json:"compatible_commercial_types,omitempty"`
}
type Organization struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
type Version struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
CreationDate time.Time `json:"creation_date,omitempty"`
ModificationDate time.Time `json:"modification_date,omitempty"`
LocalImages []*LocalImage `json:"local_images,omitempty"`
}
// Service API
type GetServiceInfoRequest struct {
}
func (s *API) GetServiceInfo(req *GetServiceInfoRequest, opts ...scw.RequestOption) (*GetServiceInfoResponse, error) {
var err error
scwReq := &scw.ScalewayRequest{
Method: "GET",
Path: "/marketplace/v1",
Headers: http.Header{},
}
var resp GetServiceInfoResponse
err = s.client.Do(scwReq, &resp, opts...)
if err != nil {
return nil, err
}
return &resp, nil
}
type ListImagesRequest struct {
PerPage *int32 `json:"-"`
Page *int32 `json:"-"`
}
func (s *API) ListImages(req *ListImagesRequest, opts ...scw.RequestOption) (*ListImagesResponse, error) {
var err error
defaultPerPage, exist := s.client.GetDefaultPageSize()
if (req.PerPage == nil || *req.PerPage == 0) && exist {
req.PerPage = &defaultPerPage
}
query := url.Values{}
parameter.AddToQuery(query, "per_page", req.PerPage)
parameter.AddToQuery(query, "page", req.Page)
scwReq := &scw.ScalewayRequest{
Method: "GET",
Path: "/marketplace/v1/images",
Query: query,
Headers: http.Header{},
}
var resp ListImagesResponse
err = s.client.Do(scwReq, &resp, opts...)
if err != nil {
return nil, err
}
return &resp, nil
}
type GetImageRequest struct {
ImageID string `json:"-"`
}
func (s *API) GetImage(req *GetImageRequest, opts ...scw.RequestOption) (*GetImageResponse, error) {
var err error
if fmt.Sprint(req.ImageID) == "" {
return nil, errors.New("field ImageID cannot be empty in request")
}
scwReq := &scw.ScalewayRequest{
Method: "GET",
Path: "/marketplace/v1/images/" + fmt.Sprint(req.ImageID) + "",
Headers: http.Header{},
}
var resp GetImageResponse
err = s.client.Do(scwReq, &resp, opts...)
if err != nil {
return nil, err
}
return &resp, nil
}
type ListVersionsRequest struct {
ImageID string `json:"-"`
}
func (s *API) ListVersions(req *ListVersionsRequest, opts ...scw.RequestOption) (*ListVersionsResponse, error) {
var err error
if fmt.Sprint(req.ImageID) == "" {
return nil, errors.New("field ImageID cannot be empty in request")
}
scwReq := &scw.ScalewayRequest{
Method: "GET",
Path: "/marketplace/v1/images/" + fmt.Sprint(req.ImageID) + "/versions",
Headers: http.Header{},
}
var resp ListVersionsResponse
err = s.client.Do(scwReq, &resp, opts...)
if err != nil {
return nil, err
}
return &resp, nil
}
type GetVersionRequest struct {
ImageID string `json:"-"`
VersionID string `json:"-"`
}
func (s *API) GetVersion(req *GetVersionRequest, opts ...scw.RequestOption) (*GetVersionResponse, error) {
var err error
if fmt.Sprint(req.ImageID) == "" {
return nil, errors.New("field ImageID cannot be empty in request")
}
if fmt.Sprint(req.VersionID) == "" {
return nil, errors.New("field VersionID cannot be empty in request")
}
scwReq := &scw.ScalewayRequest{
Method: "GET",
Path: "/marketplace/v1/images/" + fmt.Sprint(req.ImageID) + "/versions/" + fmt.Sprint(req.VersionID) + "",
Headers: http.Header{},
}
var resp GetVersionResponse
err = s.client.Do(scwReq, &resp, opts...)
if err != nil {
return nil, err
}
return &resp, nil
}

View File

@ -0,0 +1,81 @@
package marketplace
import (
"fmt"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// getLocalImage returns the correct local version of an image matching
// the current zone and the compatible commercial type
func (version *Version) getLocalImage(zone utils.Zone, commercialType string) (*LocalImage, error) {
for _, localImage := range version.LocalImages {
// Check if in correct zone
if localImage.Zone != zone {
continue
}
// Check if compatible with wanted commercial type
for _, compatibleCommercialType := range localImage.CompatibleCommercialTypes {
if compatibleCommercialType == commercialType {
return localImage, nil
}
}
}
return nil, fmt.Errorf("couldn't find compatible local image for this image version (%s)", version.ID)
}
// getLatestVersion returns the current/latests version on an image,
// or an error in case the image doesn't have a public version.
func (image *Image) getLatestVersion() (*Version, error) {
for _, version := range image.Versions {
if version.ID == image.CurrentPublicVersion {
return version, nil
}
}
return nil, fmt.Errorf("latest version could not be found for image %s", image.Name)
}
// FindLocalImageIDByName search for an image with the given name (exact match) in the given region
// it returns the latest version of this specific image.
func (s *API) FindLocalImageIDByName(imageName string, zone utils.Zone, commercialType string) (string, error) {
listImageRequest := &ListImagesRequest{}
listImageResponse, err := s.ListImages(listImageRequest)
if err != nil {
return "", err
}
// TODO: handle pagination
images := listImageResponse.Images
_ = images
for _, image := range images {
// Match name of the image
if image.Name == imageName {
latestVersion, err := image.getLatestVersion()
if err != nil {
return "", fmt.Errorf("couldn't find a matching image for the given name (%s), zone (%s) and commercial type (%s): %s", imageName, zone, commercialType, err)
}
localImage, err := latestVersion.getLocalImage(zone, commercialType)
if err != nil {
return "", fmt.Errorf("couldn't find a matching image for the given name (%s), zone (%s) and commercial type (%s): %s", imageName, zone, commercialType, err)
}
return localImage.ID, nil
}
}
return "", fmt.Errorf("couldn't find a matching image for the given name (%s), zone (%s) and commercial type (%s)", imageName, zone, commercialType)
}

View File

@ -0,0 +1,8 @@
module github.com/scaleway/scaleway-sdk-go
go 1.12
require (
github.com/dnaeon/go-vcr v1.0.1
gopkg.in/yaml.v2 v2.2.2
)

View File

@ -0,0 +1,89 @@
package async
import (
"fmt"
"time"
)
var (
defaultInterval = time.Second
defaultTimeout = time.Minute * 5
)
type IntervalStrategy func() <-chan time.Time
// WaitSyncConfig defines the waiting options.
type WaitSyncConfig struct {
// This method will be called from another goroutine.
Get func() (value interface{}, err error, isTerminal bool)
IntervalStrategy IntervalStrategy
Timeout time.Duration
}
// LinearIntervalStrategy defines a linear interval duration.
func LinearIntervalStrategy(interval time.Duration) IntervalStrategy {
return func() <-chan time.Time {
return time.After(interval)
}
}
// FibonacciIntervalStrategy defines an interval duration who follow the Fibonacci sequence.
func FibonacciIntervalStrategy(base time.Duration, factor float32) IntervalStrategy {
var x, y float32 = 0, 1
return func() <-chan time.Time {
x, y = y, x+(y*factor)
return time.After(time.Duration(x) * base)
}
}
// WaitSync waits and returns when a given stop condition is true or if an error occurs.
func WaitSync(config *WaitSyncConfig) (terminalValue interface{}, err error) {
// initialize configuration
if config.IntervalStrategy == nil {
config.IntervalStrategy = LinearIntervalStrategy(defaultInterval)
}
if config.Timeout == 0 {
config.Timeout = defaultTimeout
}
resultValue := make(chan interface{})
resultErr := make(chan error)
timeout := make(chan bool)
go func() {
for {
// get the payload
value, err, stopCondition := config.Get()
// send the payload
if err != nil {
resultErr <- err
return
}
if stopCondition {
resultValue <- value
return
}
// waiting for an interval before next get() call or a timeout
select {
case <-timeout:
return
case <-config.IntervalStrategy():
// sleep
}
}
}()
// waiting for a result or a timeout
select {
case val := <-resultValue:
return val, nil
case err := <-resultErr:
return nil, err
case <-time.After(config.Timeout):
timeout <- true
return nil, fmt.Errorf("timeout after %v", config.Timeout)
}
}

View File

@ -0,0 +1,14 @@
package auth
import "net/http"
// Auth implement methods required for authentication.
// Valid authentication are currently a token or no auth.
type Auth interface {
// Headers returns headers that must be add to the http request
Headers() http.Header
// AnonymizedHeaders returns an anonymised version of Headers()
// This method could be use for logging purpose.
AnonymizedHeaders() http.Header
}

View File

@ -0,0 +1,19 @@
package auth
import "net/http"
type noAuth struct {
}
// NewNoAuth return an auth with no authentication method
func NewNoAuth() *noAuth {
return &noAuth{}
}
func (t *noAuth) Headers() http.Header {
return http.Header{}
}
func (t *noAuth) AnonymizedHeaders() http.Header {
return http.Header{}
}

View File

@ -0,0 +1,42 @@
package auth
import "net/http"
type token struct {
accessKey string
secretKey string
}
// XAuthTokenHeader is Scaleway standard auth header
const XAuthTokenHeader = "X-Auth-Token"
// NewToken create a token authentication from an
// access key and a secret key
func NewToken(accessKey, secretKey string) *token {
return &token{accessKey: accessKey, secretKey: secretKey}
}
// Headers returns headers that must be add to the http request
func (t *token) Headers() http.Header {
headers := http.Header{}
headers.Set(XAuthTokenHeader, t.secretKey)
return headers
}
// AnonymizedHeaders returns an anonymized version of Headers()
// This method could be use for logging purpose.
func (t *token) AnonymizedHeaders() http.Header {
headers := http.Header{}
var secret string
switch {
case len(t.secretKey) == 0:
secret = ""
case len(t.secretKey) > 8:
secret = t.secretKey[0:8] + "-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
default:
secret = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
headers.Set(XAuthTokenHeader, secret)
return headers
}

View File

@ -0,0 +1,41 @@
package errors
import "fmt"
// Error is a base error that implement scw.SdkError
type Error struct {
str string
err error
}
// Error implement standard xerror.Wrapper interface
func (e *Error) Unwrap() error {
return e.err
}
// Error implement standard error interface
func (e *Error) Error() string {
str := "[scaleway-sdk-go] " + e.str
if e.err != nil {
str += ": " + e.err.Error()
}
return str
}
// IsScwSdkError implement SdkError interface
func (e *Error) IsScwSdkError() {}
// New creates a new error with that same interface as fmt.Errorf
func New(format string, args ...interface{}) *Error {
return &Error{
str: fmt.Sprintf(format, args...),
}
}
// Wrap an error with additional information
func Wrap(err error, format string, args ...interface{}) *Error {
return &Error{
err: err,
str: fmt.Sprintf(format, args...),
}
}

View File

@ -0,0 +1,139 @@
package marshaler
import (
"encoding/json"
"time"
)
// Duration implements a JSON Marshaler to encode a time.Duration in milliseconds.
type Duration int64
const milliSec = Duration(time.Millisecond)
// NewDuration converts a *time.Duration to a *Duration type.
func NewDuration(t *time.Duration) *Duration {
if t == nil {
return nil
}
d := Duration(t.Nanoseconds())
return &d
}
// Standard converts a *Duration to a *time.Duration type.
func (d *Duration) Standard() *time.Duration {
return (*time.Duration)(d)
}
// MarshalJSON encodes the Duration in milliseconds.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(int64(d / milliSec))
}
// UnmarshalJSON decodes milliseconds to Duration.
func (d *Duration) UnmarshalJSON(b []byte) error {
var tmp int64
err := json.Unmarshal(b, &tmp)
if err != nil {
return err
}
*d = Duration(tmp) * milliSec
return nil
}
// DurationSlice is a slice of *Duration
type DurationSlice []*Duration
// NewDurationSlice converts a []*time.Duration to a DurationSlice type.
func NewDurationSlice(t []*time.Duration) DurationSlice {
ds := make([]*Duration, len(t))
for i := range ds {
ds[i] = NewDuration(t[i])
}
return ds
}
// Standard converts a DurationSlice to a []*time.Duration type.
func (ds *DurationSlice) Standard() []*time.Duration {
t := make([]*time.Duration, len(*ds))
for i := range t {
t[i] = (*ds)[i].Standard()
}
return t
}
// Durationint32Map is a int32 map of *Duration
type Durationint32Map map[int32]*Duration
// NewDurationint32Map converts a map[int32]*time.Duration to a Durationint32Map type.
func NewDurationint32Map(t map[int32]*time.Duration) Durationint32Map {
dm := make(Durationint32Map, len(t))
for i := range t {
dm[i] = NewDuration(t[i])
}
return dm
}
// Standard converts a Durationint32Map to a map[int32]*time.Duration type.
func (dm *Durationint32Map) Standard() map[int32]*time.Duration {
t := make(map[int32]*time.Duration, len(*dm))
for key, value := range *dm {
t[key] = value.Standard()
}
return t
}
// LongDuration implements a JSON Marshaler to encode a time.Duration in days.
type LongDuration int64
const day = LongDuration(time.Hour) * 24
// NewLongDuration converts a *time.Duration to a *LongDuration type.
func NewLongDuration(t *time.Duration) *LongDuration {
if t == nil {
return nil
}
d := LongDuration(t.Nanoseconds())
return &d
}
// Standard converts a *LongDuration to a *time.Duration type.
func (d *LongDuration) Standard() *time.Duration {
return (*time.Duration)(d)
}
// MarshalJSON encodes the LongDuration in days.
func (d LongDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(int64(d / day))
}
// UnmarshalJSON decodes days to LongDuration.
func (d *LongDuration) UnmarshalJSON(b []byte) error {
var tmp int64
err := json.Unmarshal(b, &tmp)
if err != nil {
return err
}
*d = LongDuration(tmp) * day
return nil
}
// LongDurationSlice is a slice of *LongDuration
type LongDurationSlice []*LongDuration
// NewLongDurationSlice converts a []*time.Duration to a LongDurationSlice type.
func NewLongDurationSlice(t []*time.Duration) LongDurationSlice {
ds := make([]*LongDuration, len(t))
for i := range ds {
ds[i] = NewLongDuration(t[i])
}
return ds
}
// Standard converts a LongDurationSlice to a []*time.Duration type.
func (ds *LongDurationSlice) Standard() []*time.Duration {
t := make([]*time.Duration, len(*ds))
for i := range t {
t[i] = (*ds)[i].Standard()
}
return t
}

View File

@ -0,0 +1,31 @@
package parameter
import (
"fmt"
"net/url"
"reflect"
)
// AddToQuery add a key/value pair to an URL query
func AddToQuery(query url.Values, key string, value interface{}) {
elemValue := reflect.ValueOf(value)
if elemValue.Kind() == reflect.Invalid || elemValue.Kind() == reflect.Ptr && elemValue.IsNil() {
return
}
for elemValue.Kind() == reflect.Ptr {
elemValue = reflect.ValueOf(value).Elem()
}
elemType := elemValue.Type()
switch {
case elemType.Kind() == reflect.Slice:
for i := 0; i < elemValue.Len(); i++ {
query.Add(key, fmt.Sprint(elemValue.Index(i).Interface()))
}
default:
query.Add(key, fmt.Sprint(elemValue.Interface()))
}
}

View File

@ -0,0 +1,102 @@
package logger
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strconv"
)
var DefaultLogger = newLogger(os.Stderr, LogLevelWarning)
var logger Logger = DefaultLogger
// loggerT is the default logger used by scaleway-sdk-go.
type loggerT struct {
m [4]*log.Logger
v LogLevel
}
// Init create a new default logger.
// Not mutex-protected, should be called before any scaleway-sdk-go functions.
func (g *loggerT) Init(w io.Writer, level LogLevel) {
g.m = newLogger(w, level).m
}
// Debugf logs to the DEBUG log. Arguments are handled in the manner of fmt.Printf.
func Debugf(format string, args ...interface{}) { logger.Debugf(format, args...) }
func (g *loggerT) Debugf(format string, args ...interface{}) {
g.m[LogLevelDebug].Printf(format, args...)
}
// Infof logs to the INFO log. Arguments are handled in the manner of fmt.Printf.
func Infof(format string, args ...interface{}) { logger.Infof(format, args...) }
func (g *loggerT) Infof(format string, args ...interface{}) {
g.m[LogLevelInfo].Printf(format, args...)
}
// Warningf logs to the WARNING log. Arguments are handled in the manner of fmt.Printf.
func Warningf(format string, args ...interface{}) { logger.Warningf(format, args...) }
func (g *loggerT) Warningf(format string, args ...interface{}) {
g.m[LogLevelWarning].Printf(format, args...)
}
// Errorf logs to the ERROR log. Arguments are handled in the manner of fmt.Printf.
func Errorf(format string, args ...interface{}) { logger.Errorf(format, args...) }
func (g *loggerT) Errorf(format string, args ...interface{}) {
g.m[LogLevelError].Printf(format, args...)
}
// ShouldLog reports whether verbosity level l is at least the requested verbose level.
func ShouldLog(level LogLevel) bool { return logger.ShouldLog(level) }
func (g *loggerT) ShouldLog(level LogLevel) bool {
return level <= g.v
}
func isEnabled(envKey string) bool {
env, exist := os.LookupEnv(envKey)
if !exist {
return false
}
value, err := strconv.ParseBool(env)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: environment variable %s has invalid boolean value\n", envKey)
}
return value
}
// newLogger creates a logger to be used as default logger.
// All logs are written to w.
func newLogger(w io.Writer, level LogLevel) *loggerT {
errorW := ioutil.Discard
warningW := ioutil.Discard
infoW := ioutil.Discard
debugW := ioutil.Discard
if isEnabled("SCW_DEBUG") {
level = LogLevelDebug
}
switch level {
case LogLevelDebug:
debugW = w
case LogLevelInfo:
infoW = w
case LogLevelWarning:
warningW = w
case LogLevelError:
errorW = w
}
// Error logs will be written to errorW, warningW, infoW and debugW.
// Warning logs will be written to warningW, infoW and debugW.
// Info logs will be written to infoW and debugW.
// Debug logs will be written to debugW.
var m [4]*log.Logger
m[LogLevelError] = log.New(io.MultiWriter(debugW, infoW, warningW, errorW), severityName[LogLevelError]+": ", log.LstdFlags)
m[LogLevelWarning] = log.New(io.MultiWriter(debugW, infoW, warningW), severityName[LogLevelWarning]+": ", log.LstdFlags)
m[LogLevelInfo] = log.New(io.MultiWriter(debugW, infoW), severityName[LogLevelInfo]+": ", log.LstdFlags)
m[LogLevelDebug] = log.New(debugW, severityName[LogLevelDebug]+": ", log.LstdFlags)
return &loggerT{m: m, v: level}
}

View File

@ -0,0 +1,50 @@
package logger
import "os"
type LogLevel int
const (
// LogLevelDebug indicates Debug severity.
LogLevelDebug LogLevel = iota
// LogLevelInfo indicates Info severity.
LogLevelInfo
// LogLevelWarning indicates Warning severity.
LogLevelWarning
// LogLevelError indicates Error severity.
LogLevelError
)
// severityName contains the string representation of each severity.
var severityName = []string{
LogLevelDebug: "DEBUG",
LogLevelInfo: "INFO",
LogLevelWarning: "WARNING",
LogLevelError: "ERROR",
}
// Logger does underlying logging work for scaleway-sdk-go.
type Logger interface {
// Debugf logs to DEBUG log. Arguments are handled in the manner of fmt.Printf.
Debugf(format string, args ...interface{})
// Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf.
Infof(format string, args ...interface{})
// Warningf logs to WARNING log. Arguments are handled in the manner of fmt.Printf.
Warningf(format string, args ...interface{})
// Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf.
Errorf(format string, args ...interface{})
// ShouldLog reports whether verbosity level l is at least the requested verbose level.
ShouldLog(level LogLevel) bool
}
// SetLogger sets logger that is used in by the SDK.
// Not mutex-protected, should be called before any scaleway-sdk-go functions.
func SetLogger(l Logger) {
logger = l
}
// EnableDebugMode enable LogLevelDebug on the default logger.
// If a custom logger was provided with SetLogger this method has no effect.
func EnableDebugMode() {
DefaultLogger.Init(os.Stderr, LogLevelDebug)
}

View File

@ -0,0 +1,304 @@
package scw
import (
"crypto/tls"
"encoding/json"
"net"
"net/http"
"net/http/httputil"
"strconv"
"sync/atomic"
"time"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/logger"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// Client is the Scaleway client which performs API requests.
//
// This client should be passed in the `NewApi` functions whenever an API instance is created.
// Creating a Client is done with the `NewClient` function.
type Client struct {
httpClient httpClient
auth auth.Auth
apiURL string
userAgent string
defaultProjectID *string
defaultRegion *utils.Region
defaultZone *utils.Zone
defaultPageSize *int32
}
func defaultOptions() []ClientOption {
return []ClientOption{
WithAPIURL("https://api.scaleway.com"),
withDefaultUserAgent(userAgent),
}
}
// NewClient instantiate a new Client object.
//
// Zero or more ClientOption object can be passed as a parameter.
// These options will then be applied to the client.
func NewClient(opts ...ClientOption) (*Client, error) {
s := newSettings()
// apply options
s.apply(append(defaultOptions(), opts...))
// validate settings
err := s.validate()
if err != nil {
return nil, err
}
// dial the API
if s.httpClient == nil {
s.httpClient = newHTTPClient()
}
// insecure mode
if s.insecure {
logger.Debugf("client: using insecure mode")
setInsecureMode(s.httpClient)
}
logger.Debugf("client: using sdk version " + version)
return &Client{
auth: s.token,
httpClient: s.httpClient,
apiURL: s.apiURL,
userAgent: s.userAgent,
defaultProjectID: s.defaultProjectID,
defaultRegion: s.defaultRegion,
defaultZone: s.defaultZone,
defaultPageSize: s.defaultPageSize,
}, nil
}
// GetDefaultProjectID return the default project ID
// of the client. This value can be set in the client option
// WithDefaultProjectID(). Be aware this value can be empty.
func (c *Client) GetDefaultProjectID() (string, bool) {
if c.defaultProjectID != nil {
return *c.defaultProjectID, true
}
return "", false
}
// GetDefaultRegion return the default region of the client.
// This value can be set in the client option
// WithDefaultRegion(). Be aware this value can be empty.
func (c *Client) GetDefaultRegion() (utils.Region, bool) {
if c.defaultRegion != nil {
return *c.defaultRegion, true
}
return utils.Region(""), false
}
// GetDefaultZone return the default zone of the client.
// This value can be set in the client option
// WithDefaultZone(). Be aware this value can be empty.
func (c *Client) GetDefaultZone() (utils.Zone, bool) {
if c.defaultZone != nil {
return *c.defaultZone, true
}
return utils.Zone(""), false
}
// GetDefaultPageSize return the default page size of the client.
// This value can be set in the client option
// WithDefaultPageSize(). Be aware this value can be empty.
func (c *Client) GetDefaultPageSize() (int32, bool) {
if c.defaultPageSize != nil {
return *c.defaultPageSize, true
}
return 0, false
}
// Do performs HTTP request(s) based on the ScalewayRequest object.
// RequestOptions are applied prior to doing the request.
func (c *Client) Do(req *ScalewayRequest, res interface{}, opts ...RequestOption) (err error) {
requestSettings := newRequestSettings()
// apply request options
requestSettings.apply(opts)
// validate request options
err = requestSettings.validate()
if err != nil {
return err
}
if requestSettings.ctx != nil {
req.Ctx = requestSettings.ctx
}
if requestSettings.allPages {
return c.doListAll(req, res)
}
return c.do(req, res)
}
// requestNumber auto increments on each do().
// This allows easy distinguishing of concurrently performed requests in log.
var requestNumber uint32
// do performs a single HTTP request based on the ScalewayRequest object.
func (c *Client) do(req *ScalewayRequest, res interface{}) (sdkErr SdkError) {
currentRequestNumber := atomic.AddUint32(&requestNumber, 1)
if req == nil {
return errors.New("request must be non-nil")
}
// build url
url, sdkErr := req.getURL(c.apiURL)
if sdkErr != nil {
return sdkErr
}
logger.Debugf("creating %s request on %s", req.Method, url.String())
// build request
httpRequest, err := http.NewRequest(req.Method, url.String(), req.Body)
if err != nil {
return errors.Wrap(err, "could not create request")
}
httpRequest.Header = req.getAllHeaders(c.auth, c.userAgent, false)
if req.Ctx != nil {
httpRequest = httpRequest.WithContext(req.Ctx)
}
if logger.ShouldLog(logger.LogLevelDebug) {
// Keep original headers (before anonymization)
originalHeaders := httpRequest.Header
// Get anonymized headers
httpRequest.Header = req.getAllHeaders(c.auth, c.userAgent, true)
dump, err := httputil.DumpRequestOut(httpRequest, true)
if err != nil {
logger.Warningf("cannot dump outgoing request: %s", err)
} else {
var logString string
logString += "\n--------------- Scaleway SDK REQUEST %d : ---------------\n"
logString += "%s\n"
logString += "---------------------------------------------------------"
logger.Debugf(logString, currentRequestNumber, dump)
}
// Restore original headers before sending the request
httpRequest.Header = originalHeaders
}
// execute request
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return errors.Wrap(err, "error executing request")
}
defer func() {
closeErr := httpResponse.Body.Close()
if sdkErr == nil && closeErr != nil {
sdkErr = errors.Wrap(closeErr, "could not close http response")
}
}()
if logger.ShouldLog(logger.LogLevelDebug) {
dump, err := httputil.DumpResponse(httpResponse, true)
if err != nil {
logger.Warningf("cannot dump ingoing response: %s", err)
} else {
var logString string
logString += "\n--------------- Scaleway SDK RESPONSE %d : ---------------\n"
logString += "%s\n"
logString += "----------------------------------------------------------"
logger.Debugf(logString, currentRequestNumber, dump)
}
}
sdkErr = hasResponseError(httpResponse)
if sdkErr != nil {
return sdkErr
}
if res != nil {
err = json.NewDecoder(httpResponse.Body).Decode(&res)
if err != nil {
return errors.Wrap(err, "could not parse response body")
}
// Handle instance API X-Total-Count header
xTotalCountStr := httpResponse.Header.Get("X-Total-Count")
if legacyLister, isLegacyLister := res.(legacyLister); isLegacyLister && xTotalCountStr != "" {
xTotalCount, err := strconv.Atoi(xTotalCountStr)
if err != nil {
return errors.Wrap(err, "could not parse X-Total-Count header")
}
legacyLister.UnsafeSetTotalCount(xTotalCount)
}
}
return nil
}
func newHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
MaxIdleConnsPerHost: 20,
},
}
}
func setInsecureMode(c httpClient) {
standardHTTPClient, ok := c.(*http.Client)
if !ok {
logger.Warningf("client: cannot use insecure mode with HTTP client of type %T", c)
return
}
transportClient, ok := standardHTTPClient.Transport.(*http.Transport)
if !ok {
logger.Warningf("client: cannot use insecure mode with Transport client of type %T", standardHTTPClient.Transport)
return
}
if transportClient.TLSClientConfig == nil {
transportClient.TLSClientConfig = &tls.Config{}
}
transportClient.TLSClientConfig.InsecureSkipVerify = true
}
func hasResponseError(res *http.Response) SdkError {
if res.StatusCode >= 200 && res.StatusCode <= 299 {
return nil
}
newErr := &ResponseError{
StatusCode: res.StatusCode,
Status: res.Status,
}
if res.Body == nil {
return newErr
}
err := json.NewDecoder(res.Body).Decode(newErr)
if err != nil {
return errors.Wrap(err, "could not parse error response body")
}
return newErr
}

View File

@ -0,0 +1,142 @@
package scw
import (
"net/http"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/scwconfig"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// ClientOption is a function which applies options to a settings object.
type ClientOption func(*settings)
// httpClient wraps the net/http Client Do method
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// WithHTTPClient client option allows passing a custom http.Client which will be used for all requests.
func WithHTTPClient(httpClient httpClient) ClientOption {
return func(s *settings) {
s.httpClient = httpClient
}
}
// WithoutAuth client option sets the client token to an empty token.
func WithoutAuth() ClientOption {
return func(s *settings) {
s.token = auth.NewNoAuth()
}
}
// WithAuth client option sets the client access key and secret key.
func WithAuth(accessKey, secretKey string) ClientOption {
return func(s *settings) {
s.token = auth.NewToken(accessKey, secretKey)
}
}
// WithAPIURL client option overrides the API URL of the Scaleway API to the given URL.
func WithAPIURL(apiURL string) ClientOption {
return func(s *settings) {
s.apiURL = apiURL
}
}
// WithInsecure client option enables insecure transport on the client.
func WithInsecure() ClientOption {
return func(s *settings) {
s.insecure = true
}
}
// WithUserAgent client option append a user agent to the default user agent of the SDK.
func WithUserAgent(ua string) ClientOption {
return func(s *settings) {
if s.userAgent != "" && ua != "" {
s.userAgent += " "
}
s.userAgent += ua
}
}
// withDefaultUserAgent client option overrides the default user agent of the SDK.
func withDefaultUserAgent(ua string) ClientOption {
return func(s *settings) {
s.userAgent = ua
}
}
// WithConfig client option configure a client with Scaleway configuration.
func WithConfig(config scwconfig.Config) ClientOption {
return func(s *settings) {
// The access key is not used for API authentications.
accessKey, _ := config.GetAccessKey()
secretKey, secretKeyExist := config.GetSecretKey()
if secretKeyExist {
s.token = auth.NewToken(accessKey, secretKey)
}
apiURL, exist := config.GetAPIURL()
if exist {
s.apiURL = apiURL
}
insecure, exist := config.GetInsecure()
if exist {
s.insecure = insecure
}
defaultProjectID, exist := config.GetDefaultProjectID()
if exist {
s.defaultProjectID = &defaultProjectID
}
defaultRegion, exist := config.GetDefaultRegion()
if exist {
s.defaultRegion = &defaultRegion
}
defaultZone, exist := config.GetDefaultZone()
if exist {
s.defaultZone = &defaultZone
}
}
}
// WithDefaultProjectID client option sets the client default project ID.
//
// It will be used as the default value of the project_id field in all requests made with this client.
func WithDefaultProjectID(projectID string) ClientOption {
return func(s *settings) {
s.defaultProjectID = &projectID
}
}
// WithDefaultRegion client option sets the client default region.
//
// It will be used as the default value of the region field in all requests made with this client.
func WithDefaultRegion(region utils.Region) ClientOption {
return func(s *settings) {
s.defaultRegion = &region
}
}
// WithDefaultZone client option sets the client default zone.
//
// It will be used as the default value of the zone field in all requests made with this client.
func WithDefaultZone(zone utils.Zone) ClientOption {
return func(s *settings) {
s.defaultZone = &zone
}
}
// WithDefaultPageSize client option overrides the default page size of the SDK.
//
// It will be used as the default value of the page_size field in all requests made with this client.
func WithDefaultPageSize(pageSize int32) ClientOption {
return func(s *settings) {
s.defaultPageSize = &pageSize
}
}

View File

@ -0,0 +1,46 @@
package scw
import (
"fmt"
)
// SdkError is a base interface for all Scaleway SDK errors.
type SdkError interface {
Error() string
IsScwSdkError()
}
// ResponseError is an error type for the Scaleway API
type ResponseError struct {
// Message is a human-friendly error message
Message string `json:"message"`
// Type is a string code that defines the kind of error
Type string `json:"type,omitempty"`
// Fields contains detail about validation error
Fields map[string][]string `json:"fields,omitempty"`
// StatusCode is the HTTP status code received
StatusCode int `json:"-"`
// Status is the HTTP status received
Status string `json:"-"`
}
func (e *ResponseError) Error() string {
s := fmt.Sprintf("scaleway-sdk-go: http error %s", e.Status)
if e.Message != "" {
s = fmt.Sprintf("%s: %s", s, e.Message)
}
if len(e.Fields) > 0 {
s = fmt.Sprintf("%s: %v", s, e.Fields)
}
return s
}
// IsScwSdkError implement SdkError interface
func (e *ResponseError) IsScwSdkError() {}

View File

@ -0,0 +1,64 @@
package scw
import (
"math"
"reflect"
"strconv"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
)
type lister interface {
UnsafeGetTotalCount() int
UnsafeAppend(interface{}) (int, SdkError)
}
type legacyLister interface {
UnsafeSetTotalCount(totalCount int)
}
// doListAll collects all pages of a List request and aggregate all results on a single response.
func (c *Client) doListAll(req *ScalewayRequest, res interface{}) (err SdkError) {
// check for lister interface
if response, isLister := res.(lister); isLister {
pageCount := math.MaxUint32
for page := 1; page <= pageCount; page++ {
// set current page
req.Query.Set("page", strconv.Itoa(page))
// request the next page
nextPage := newPage(response)
err := c.do(req, nextPage)
if err != nil {
return err
}
// append results
pageSize, err := response.UnsafeAppend(nextPage)
if err != nil {
return err
}
if pageSize == 0 {
return nil
}
// set total count on first request
if pageCount == math.MaxUint32 {
totalCount := nextPage.(lister).UnsafeGetTotalCount()
pageCount = (totalCount + pageSize - 1) / pageSize
}
}
return nil
}
return errors.New("%T does not support pagination", res)
}
// newPage returns a variable set to the zero value of the given type
func newPage(v interface{}) interface{} {
// reflect.New always create a pointer, that's why we use reflect.Indirect before
return reflect.New(reflect.Indirect(reflect.ValueOf(v)).Type()).Interface()
}

View File

@ -0,0 +1,82 @@
package scw
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/utils"
)
// ScalewayRequest contains all the contents related to performing a request on the Scaleway API.
type ScalewayRequest struct {
Method string
Path string
Headers http.Header
Query url.Values
Body io.Reader
Ctx context.Context
}
// getAllHeaders constructs a http.Header object and aggregates all headers into the object.
func (req *ScalewayRequest) getAllHeaders(token auth.Auth, userAgent string, anonymized bool) http.Header {
var allHeaders http.Header
if anonymized {
allHeaders = token.AnonymizedHeaders()
} else {
allHeaders = token.Headers()
}
allHeaders.Set("User-Agent", userAgent)
if req.Body != nil {
allHeaders.Set("content-type", "application/json")
}
for key, value := range req.Headers {
for _, v := range value {
allHeaders.Set(key, v)
}
}
return allHeaders
}
// getURL constructs a URL based on the base url and the client.
func (req *ScalewayRequest) getURL(baseURL string) (*url.URL, SdkError) {
url, err := url.Parse(baseURL + req.Path)
if err != nil {
return nil, errors.New("invalid url %s: %s", baseURL+req.Path, err)
}
url.RawQuery = req.Query.Encode()
return url, nil
}
// SetBody json marshal the given body and write the json content type
// to the request. It also catches when body is a file.
func (req *ScalewayRequest) SetBody(body interface{}) error {
var contentType string
var content io.Reader
switch b := body.(type) {
case *utils.File:
contentType = b.ContentType
content = b.Content
default:
buf, err := json.Marshal(body)
if err != nil {
return err
}
contentType = "application/json"
content = bytes.NewReader(buf)
}
req.Headers.Set("Content-Type", contentType)
req.Body = content
return nil
}

View File

@ -0,0 +1,43 @@
package scw
import (
"context"
)
// RequestOption is a function that applies options to a ScalewayRequest.
type RequestOption func(*requestSettings)
// WithContext request option sets the context of a ScalewayRequest
func WithContext(ctx context.Context) RequestOption {
return func(s *requestSettings) {
s.ctx = ctx
}
}
// WithAllPages aggregate all pages in the response of a List request.
// Will error when pagination is not supported on the request.
func WithAllPages() RequestOption {
return func(s *requestSettings) {
s.allPages = true
}
}
type requestSettings struct {
ctx context.Context
allPages bool
}
func newRequestSettings() *requestSettings {
return &requestSettings{}
}
func (s *requestSettings) apply(opts []RequestOption) {
for _, opt := range opts {
opt(s)
}
}
func (s *requestSettings) validate() SdkError {
// nothing so far
return nil
}

View File

@ -0,0 +1,64 @@
package scw
import (
"fmt"
"net/url"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/utils"
)
type settings struct {
apiURL string
token auth.Auth
userAgent string
httpClient httpClient
insecure bool
defaultProjectID *string
defaultRegion *utils.Region
defaultZone *utils.Zone
defaultPageSize *int32
}
func newSettings() *settings {
return &settings{}
}
func (s *settings) apply(opts []ClientOption) {
for _, opt := range opts {
opt(s)
}
}
func (s *settings) validate() error {
var err error
if s.token == nil {
return fmt.Errorf("no credential option provided")
}
_, err = url.Parse(s.apiURL)
if err != nil {
return fmt.Errorf("invalid url %s: %s", s.apiURL, err)
}
// TODO: Check ProjectID format
if s.defaultProjectID != nil && *s.defaultProjectID == "" {
return fmt.Errorf("default project id cannot be empty")
}
// TODO: Check Region format
if s.defaultRegion != nil && *s.defaultRegion == "" {
return fmt.Errorf("default region cannot be empty")
}
// TODO: Check Zone format
if s.defaultZone != nil && *s.defaultZone == "" {
return fmt.Errorf("default zone cannot be empty")
}
if s.defaultPageSize != nil && *s.defaultPageSize <= 0 {
return fmt.Errorf("default page size cannot be <= 0")
}
return nil
}

View File

@ -0,0 +1,11 @@
package scw
import (
"fmt"
"runtime"
)
// TODO: versionning process
const version = "0.0.0"
var userAgent = fmt.Sprintf("scaleway-sdk-go/%s (%s; %s; %s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)

View File

@ -0,0 +1,48 @@
# Scaleway config
## TL;DR
Recommended config file:
```yaml
# get your credentials on https://console.scaleway.com/account/credentials
access_key: SCWXXXXXXXXXXXXXXXXX
secret_key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
default_project_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
default_region: fr-par
default_zone: fr-par-1
```
## Config file path
This package will try to locate the config file in the following ways:
1. Custom directory: `$SCW_CONFIG_PATH`
2. [XDG base directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html): `$XDG_CONFIG_HOME/scw/config.yaml`
3. Home directory: `$HOME/.config/scw/config.yaml` (`%USERPROFILE%/.config/scw/config.yaml` on windows)
## V1 config (DEPRECATED)
The V1 config `.scwrc` is supported but deprecated.
When found in the home directory, the V1 config is automatically migrated to a V2 config file in `$HOME/.config/scw/config.yaml`.
## Reading config order
When getting the value of a config field, the following priority order will be respected:
1. Environment variable
2. Legacy environment variable
3. Config file V2
4. Config file V1
## Environment variables
| Variable | Description | Legacy variables |
| :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ |
| `$SCW_ACCESS_KEY` | Access key of a token ([get yours](https://console.scaleway.com/account/credentials)) | `$SCALEWAY_ACCESS_KEY` (used by terraform) |
| `$SCW_SECRET_KEY` | Secret key of a token ([get yours](https://console.scaleway.com/account/credentials)) | `$SCW_TOKEN` (used by cli), `$SCALEWAY_TOKEN` (used by terraform), `$SCALEWAY_ACCESS_KEY` (used by terraform) |
| `$SCW_DEFAULT_PROJECT_ID` | Your default project ID, if you don't have one use your organization ID ([get yours](https://console.scaleway.com/account/credentials)) | `$SCW_ORGANIZATION` (used by cli),`$SCALEWAY_ORGANIZATION` (used by terraform) |
| `$SCW_DEFAULT_REGION` | Your default [region](https://developers.scaleway.com/en/quickstart/#region-and-zone) | `$SCW_REGION` (used by cli),`$SCALEWAY_REGION` (used by terraform) |
| `$SCW_DEFAULT_ZONE` | Your default [availability zone](https://developers.scaleway.com/en/quickstart/#region-and-zone) | `$SCW_ZONE` (used by cli),`$SCALEWAY_ZONE` (used by terraform) |
| `$SCW_API_URL` | Url of the API | - |
| `$SCW_INSECURE` | Set this to `true` to enable the insecure mode | `$SCW_TLSVERIFY` (inverse flag used by the cli) |

View File

@ -0,0 +1,423 @@
package scwconfig
import (
"encoding/json"
"fmt"
"os"
"strconv"
"github.com/scaleway/scaleway-sdk-go/logger"
"github.com/scaleway/scaleway-sdk-go/utils"
"gopkg.in/yaml.v2"
)
// Environment variables
const (
// Up-to-date
scwConfigPathEnv = "SCW_CONFIG_PATH"
scwAccessKeyEnv = "SCW_ACCESS_KEY"
scwSecretKeyEnv = "SCW_SECRET_KEY"
scwActiveProfileEnv = "SCW_PROFILE"
scwAPIURLEnv = "SCW_API_URL"
scwInsecureEnv = "SCW_INSECURE"
scwDefaultProjectIDEnv = "SCW_DEFAULT_PROJECT_ID"
scwDefaultRegionEnv = "SCW_DEFAULT_REGION"
scwDefaultZoneEnv = "SCW_DEFAULT_ZONE"
// All deprecated (cli&terraform)
terraformAccessKeyEnv = "SCALEWAY_ACCESS_KEY" // used both as access key and secret key
terraformSecretKeyEnv = "SCALEWAY_TOKEN"
terraformOrganizationEnv = "SCALEWAY_ORGANIZATION"
terraformRegionEnv = "SCALEWAY_REGION"
cliTLSVerifyEnv = "SCW_TLSVERIFY"
cliOrganizationEnv = "SCW_ORGANIZATION"
cliRegionEnv = "SCW_REGION"
cliSecretKeyEnv = "SCW_TOKEN"
// TBD
//cliVerboseEnv = "SCW_VERBOSE_API"
//cliDebugEnv = "DEBUG"
//cliNoCheckVersionEnv = "SCW_NOCHECKVERSION"
//cliTestWithRealAPIEnv = "TEST_WITH_REAL_API"
//cliSecureExecEnv = "SCW_SECURE_EXEC"
//cliGatewayEnv = "SCW_GATEWAY"
//cliSensitiveEnv = "SCW_SENSITIVE"
//cliAccountAPIEnv = "SCW_ACCOUNT_API"
//cliMetadataAPIEnv = "SCW_METADATA_API"
//cliMarketPlaceAPIEnv = "SCW_MARKETPLACE_API"
//cliComputePar1APIEnv = "SCW_COMPUTE_PAR1_API"
//cliComputeAms1APIEnv = "SCW_COMPUTE_AMS1_API"
//cliCommercialTypeEnv = "SCW_COMMERCIAL_TYPE"
//cliTargetArchEnv = "SCW_TARGET_ARCH"
)
// Config interface is made of getters to retrieve
// the config field by field.
type Config interface {
GetAccessKey() (accessKey string, exist bool)
GetSecretKey() (secretKey string, exist bool)
GetAPIURL() (apiURL string, exist bool)
GetInsecure() (insecure bool, exist bool)
GetDefaultProjectID() (defaultProjectID string, exist bool)
GetDefaultRegion() (defaultRegion utils.Region, exist bool)
GetDefaultZone() (defaultZone utils.Zone, exist bool)
}
type configV2 struct {
profile `yaml:",inline"`
ActiveProfile *string `yaml:"active_profile,omitempty"`
Profiles map[string]*profile `yaml:"profiles,omitempty"`
// withProfile is used by LoadWithProfile to handle the following priority order:
// c.withProfile > os.Getenv("SCW_PROFILE") > c.ActiveProfile
withProfile string
}
type profile struct {
AccessKey *string `yaml:"access_key,omitempty"`
SecretKey *string `yaml:"secret_key,omitempty"`
APIURL *string `yaml:"api_url,omitempty"`
Insecure *bool `yaml:"insecure,omitempty"`
DefaultProjectID *string `yaml:"default_project_id,omitempty"`
DefaultRegion *string `yaml:"default_region,omitempty"`
DefaultZone *string `yaml:"default_zone,omitempty"`
}
func unmarshalConfV2(content []byte) (*configV2, error) {
var config configV2
err := yaml.Unmarshal(content, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func (c *configV2) catchInvalidProfile() (*configV2, error) {
activeProfile, err := c.getActiveProfile()
if err != nil {
return nil, err
}
if activeProfile == "" {
return c, nil
}
_, exist := c.Profiles[activeProfile]
if !exist {
return nil, fmt.Errorf("profile %s does not exist %s", activeProfile, inConfigFile())
}
return c, nil
}
func (c *configV2) getActiveProfile() (string, error) {
switch {
case c.withProfile != "":
return c.withProfile, nil
case os.Getenv(scwActiveProfileEnv) != "":
return os.Getenv(scwActiveProfileEnv), nil
case c.ActiveProfile != nil:
if *c.ActiveProfile == "" {
return "", fmt.Errorf("active_profile key cannot be empty %s", inConfigFile())
}
return *c.ActiveProfile, nil
default:
return "", nil
}
}
// GetAccessKey retrieve the access key from the config.
// It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetAccessKey() (string, bool) {
envValue, _, envExist := getenv(scwAccessKeyEnv, terraformAccessKeyEnv)
activeProfile, _ := c.getActiveProfile()
var accessKey string
switch {
case envExist:
accessKey = envValue
case activeProfile != "" && c.Profiles[activeProfile].AccessKey != nil:
accessKey = *c.Profiles[activeProfile].AccessKey
case c.AccessKey != nil:
accessKey = *c.AccessKey
default:
logger.Warningf("no access key found")
return "", false
}
if accessKey == "" {
logger.Warningf("access key is empty")
}
return accessKey, true
}
// GetSecretKey retrieve the secret key from the config.
// It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetSecretKey() (string, bool) {
envValue, _, envExist := getenv(scwSecretKeyEnv, cliSecretKeyEnv, terraformSecretKeyEnv, terraformAccessKeyEnv)
activeProfile, _ := c.getActiveProfile()
var secretKey string
switch {
case envExist:
secretKey = envValue
case activeProfile != "" && c.Profiles[activeProfile].SecretKey != nil:
secretKey = *c.Profiles[activeProfile].SecretKey
case c.SecretKey != nil:
secretKey = *c.SecretKey
default:
logger.Warningf("no secret key found")
return "", false
}
if secretKey == "" {
logger.Warningf("secret key is empty")
}
return secretKey, true
}
// GetAPIURL retrieve the api url from the config.
// It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetAPIURL() (string, bool) {
envValue, _, envExist := getenv(scwAPIURLEnv)
activeProfile, _ := c.getActiveProfile()
var apiURL string
switch {
case envExist:
apiURL = envValue
case activeProfile != "" && c.Profiles[activeProfile].APIURL != nil:
apiURL = *c.Profiles[activeProfile].APIURL
case c.APIURL != nil:
apiURL = *c.APIURL
default:
return "", false
}
if apiURL == "" {
logger.Warningf("api URL is empty")
}
return apiURL, true
}
// GetInsecure retrieve the insecure flag from the config.
// It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetInsecure() (bool, bool) {
envValue, envKey, envExist := getenv(scwInsecureEnv, cliTLSVerifyEnv)
activeProfile, _ := c.getActiveProfile()
var insecure bool
var err error
switch {
case envExist:
insecure, err = strconv.ParseBool(envValue)
if err != nil {
logger.Warningf("env variable %s cannot be parsed: %s is invalid boolean ", envKey, envValue)
return false, false
}
if envKey == cliTLSVerifyEnv {
insecure = !insecure // TLSVerify is the inverse of Insecure
}
case activeProfile != "" && c.Profiles[activeProfile].Insecure != nil:
insecure = *c.Profiles[activeProfile].Insecure
case c.Insecure != nil:
insecure = *c.Insecure
default:
return false, false
}
return insecure, true
}
// GetDefaultProjectID retrieve the default project ID
// from the config. Legacy configs used the name
// "organization ID" or "organization" for
// this field. It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetDefaultProjectID() (string, bool) {
envValue, _, envExist := getenv(scwDefaultProjectIDEnv, cliOrganizationEnv, terraformOrganizationEnv)
activeProfile, _ := c.getActiveProfile()
var defaultProj string
switch {
case envExist:
defaultProj = envValue
case activeProfile != "" && c.Profiles[activeProfile].DefaultProjectID != nil:
defaultProj = *c.Profiles[activeProfile].DefaultProjectID
case c.DefaultProjectID != nil:
defaultProj = *c.DefaultProjectID
default:
return "", false
}
// todo: validate format
if defaultProj == "" {
logger.Warningf("default project ID is empty")
}
return defaultProj, true
}
// GetDefaultRegion retrieve the default region
// from the config. It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetDefaultRegion() (utils.Region, bool) {
envValue, _, envExist := getenv(scwDefaultRegionEnv, cliRegionEnv, terraformRegionEnv)
activeProfile, _ := c.getActiveProfile()
var defaultRegion string
switch {
case envExist:
defaultRegion = v1RegionToV2(envValue)
case activeProfile != "" && c.Profiles[activeProfile].DefaultRegion != nil:
defaultRegion = *c.Profiles[activeProfile].DefaultRegion
case c.DefaultRegion != nil:
defaultRegion = *c.DefaultRegion
default:
return "", false
}
// todo: validate format
if defaultRegion == "" {
logger.Warningf("default region is empty")
}
return utils.Region(defaultRegion), true
}
// GetDefaultZone retrieve the default zone
// from the config. It will check the following order:
// env, legacy env, active profile, default profile
//
// If the config is present in one of the above environment the
// value (which may be empty) is returned and the boolean is true.
// Otherwise the returned value will be empty and the boolean will
// be false.
func (c *configV2) GetDefaultZone() (utils.Zone, bool) {
envValue, _, envExist := getenv(scwDefaultZoneEnv)
activeProfile, _ := c.getActiveProfile()
var defaultZone string
switch {
case envExist:
defaultZone = envValue
case activeProfile != "" && c.Profiles[activeProfile].DefaultZone != nil:
defaultZone = *c.Profiles[activeProfile].DefaultZone
case c.DefaultZone != nil:
defaultZone = *c.DefaultZone
default:
return "", false
}
// todo: validate format
if defaultZone == "" {
logger.Warningf("default zone is empty")
}
return utils.Zone(defaultZone), true
}
func getenv(upToDateKey string, deprecatedKeys ...string) (string, string, bool) {
value, exist := os.LookupEnv(upToDateKey)
if exist {
logger.Infof("reading value from %s", upToDateKey)
return value, upToDateKey, true
}
for _, key := range deprecatedKeys {
value, exist := os.LookupEnv(key)
if exist {
logger.Infof("reading value from %s", key)
logger.Warningf("%s is deprecated, please use %s instead", key, upToDateKey)
return value, key, true
}
}
return "", "", false
}
const (
v1RegionFrPar = "par1"
v1RegionNlAms = "ams1"
)
// configV1 is a Scaleway CLI configuration file
type configV1 struct {
// Organization is the identifier of the Scaleway organization
Organization string `json:"organization"`
// Token is the authentication token for the Scaleway organization
Token string `json:"token"`
// Version is the actual version of scw CLI
Version string `json:"version"`
}
func unmarshalConfV1(content []byte) (*configV1, error) {
var config configV1
err := json.Unmarshal(content, &config)
if err != nil {
return nil, err
}
return &config, err
}
func (v1 *configV1) toV2() *configV2 {
return &configV2{
profile: profile{
DefaultProjectID: &v1.Organization,
SecretKey: &v1.Token,
// ignore v1 version
},
}
}
func v1RegionToV2(region string) string {
switch region {
case v1RegionFrPar:
logger.Warningf("par1 is a deprecated name for region, use fr-par instead")
return "fr-par"
case v1RegionNlAms:
logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead")
return "nl-ams"
default:
return region
}
}

View File

@ -0,0 +1,104 @@
package scwconfig
import (
"fmt"
"io/ioutil"
"os"
"github.com/scaleway/scaleway-sdk-go/logger"
)
const (
documentationLink = "https://github.com/scaleway/scaleway-sdk-go/blob/master/scwconfig/README.md"
)
// LoadWithProfile call Load() and set withProfile with the profile name.
func LoadWithProfile(profileName string) (Config, error) {
config, err := Load()
if err != nil {
return nil, err
}
v2Loaded := config.(*configV2)
v2Loaded.withProfile = profileName
return v2Loaded.catchInvalidProfile()
}
// Load config in the following order:
// - config file from SCW_CONFIG_PATH (V2 or V1)
// - config file V2
// - config file V1
// When the latest is found it migrates the V1 config
// to a V2 config following the V2 config path.
func Load() (Config, error) {
// STEP 1: try to load config file from SCW_CONFIG_PATH
configPath := os.Getenv(scwConfigPathEnv)
if configPath != "" {
content, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("cannot read config file %s: %s", scwConfigPathEnv, err)
}
confV1, err := unmarshalConfV1(content)
if err == nil {
// do not migrate automatically when using SCW_CONFIG_PATH
logger.Warningf("loaded config V1 from %s: config V1 is deprecated, please switch your config file to the V2: %s", configPath, documentationLink)
return confV1.toV2().catchInvalidProfile()
}
confV2, err := unmarshalConfV2(content)
if err != nil {
return nil, fmt.Errorf("content of config file %s is invalid: %s", configPath, err)
}
logger.Infof("successfully loaded config V2 from %s", configPath)
return confV2.catchInvalidProfile()
}
// STEP 2: try to load config file V2
v2Path, v2PathOk := GetConfigV2FilePath()
if v2PathOk && fileExist(v2Path) {
file, err := ioutil.ReadFile(v2Path)
if err != nil {
return nil, fmt.Errorf("cannot read config file: %s", err)
}
confV2, err := unmarshalConfV2(file)
if err != nil {
return nil, fmt.Errorf("content of config file %s is invalid: %s", v2Path, err)
}
logger.Infof("successfully loaded config V2 from %s", v2Path)
return confV2.catchInvalidProfile()
}
// STEP 3: try to load config file V1
logger.Debugf("no config V2 found, fall back to config V1")
v1Path, v1PathOk := GetConfigV1FilePath()
if !v1PathOk {
logger.Infof("config file not found: no home directory")
return (&configV2{}).catchInvalidProfile()
}
file, err := ioutil.ReadFile(v1Path)
if err != nil {
logger.Infof("cannot read config file: %s", err)
return (&configV2{}).catchInvalidProfile() // ignore if file doesn't exist
}
confV1, err := unmarshalConfV1(file)
if err != nil {
return nil, fmt.Errorf("content of config file %s is invalid json: %s", v1Path, err)
}
// STEP 4: migrate V1 config to V2 config file
if v2PathOk {
err = migrateV1toV2(confV1, v2Path)
if err != nil {
return nil, err
}
}
return confV1.toV2().catchInvalidProfile()
}
func fileExist(name string) bool {
_, err := os.Stat(name)
return err == nil
}

View File

@ -0,0 +1,47 @@
package scwconfig
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/scaleway/scaleway-sdk-go/logger"
"gopkg.in/yaml.v2"
)
const (
defaultConfigPermission = 0600
)
// migrateV1toV2 converts the V1 config to V2 config and save it in the target path
// use config.Save() when the method is public
func migrateV1toV2(configV1 *configV1, targetPath string) error {
// STEP 0: get absolute target path
targetPath = filepath.Clean(targetPath)
// STEP 1: create dir
err := os.MkdirAll(filepath.Dir(targetPath), 0700)
if err != nil {
logger.Debugf("mkdir did not work on %s: %s", filepath.Dir(targetPath), err)
return nil
}
// STEP 2: marshal yaml config
newConfig := configV1.toV2()
file, err := yaml.Marshal(newConfig)
if err != nil {
return err
}
// STEP 3: save config
err = ioutil.WriteFile(targetPath, file, defaultConfigPermission)
if err != nil {
logger.Debugf("cannot write file %s: %s", targetPath, err)
return nil
}
// STEP 4: log success
logger.Infof("config successfully migrated to %s", targetPath)
return nil
}

View File

@ -0,0 +1,71 @@
package scwconfig
import (
"errors"
"os"
"path/filepath"
)
const (
unixHomeDirEnv = "HOME"
windowsHomeDirEnv = "USERPROFILE"
xdgConfigDirEnv = "XDG_CONFIG_HOME"
defaultConfigFileName = "config.yaml"
)
var (
// ErrNoHomeDir errors when no user directory is found
ErrNoHomeDir = errors.New("user home directory not found")
)
func inConfigFile() string {
v2path, exist := GetConfigV2FilePath()
if exist {
return "in config file " + v2path
}
return ""
}
// GetConfigV2FilePath returns the path to the Scaleway CLI config file
func GetConfigV2FilePath() (string, bool) {
configDir, err := GetScwConfigDir()
if err != nil {
return "", false
}
return filepath.Join(configDir, defaultConfigFileName), true
}
// GetConfigV1FilePath returns the path to the Scaleway CLI config file
func GetConfigV1FilePath() (string, bool) {
path, err := GetHomeDir()
if err != nil {
return "", false
}
return filepath.Join(path, ".scwrc"), true
}
// GetScwConfigDir returns the path to scw config folder
func GetScwConfigDir() (string, error) {
if xdgPath := os.Getenv(xdgConfigDirEnv); xdgPath != "" {
return filepath.Join(xdgPath, "scw"), nil
}
homeDir, err := GetHomeDir()
if err != nil {
return "", err
}
return filepath.Join(homeDir, ".config", "scw"), nil
}
// GetHomeDir returns the path to your home directory
func GetHomeDir() (string, error) {
switch {
case os.Getenv(unixHomeDirEnv) != "":
return os.Getenv(unixHomeDirEnv), nil
case os.Getenv(windowsHomeDirEnv) != "":
return os.Getenv(windowsHomeDirEnv), nil
default:
return "", ErrNoHomeDir
}
}

View File

@ -0,0 +1,158 @@
package utils
import "time"
// String returns a pointer to the string value passed in.
func String(v string) *string {
return &v
}
// StringSlice converts a slice of string values into a slice of
// string pointers
func StringSlice(src []string) []*string {
dst := make([]*string, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Strings returns a pointer to the []string value passed in.
func Strings(v []string) *[]string {
return &v
}
// StringsSlice converts a slice of []string values into a slice of
// []string pointers
func StringsSlice(src [][]string) []*[]string {
dst := make([]*[]string, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Bytes returns a pointer to the []byte value passed in.
func Bytes(v []byte) *[]byte {
return &v
}
// BytesSlice converts a slice of []byte values into a slice of
// []byte pointers
func BytesSlice(src [][]byte) []*[]byte {
dst := make([]*[]byte, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Bool returns a pointer to the bool value passed in.
func Bool(v bool) *bool {
return &v
}
// BoolSlice converts a slice of bool values into a slice of
// bool pointers
func BoolSlice(src []bool) []*bool {
dst := make([]*bool, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Int32 returns a pointer to the int32 value passed in.
func Int32(v int32) *int32 {
return &v
}
// Int32Slice converts a slice of int32 values into a slice of
// int32 pointers
func Int32Slice(src []int32) []*int32 {
dst := make([]*int32, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Int64 returns a pointer to the int64 value passed in.
func Int64(v int64) *int64 {
return &v
}
// Int64Slice converts a slice of int64 values into a slice of
// int64 pointers
func Int64Slice(src []int64) []*int64 {
dst := make([]*int64, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Uint32 returns a pointer to the uint32 value passed in.
func Uint32(v uint32) *uint32 {
return &v
}
// Uint32Slice converts a slice of uint32 values into a slice of
// uint32 pointers
func Uint32Slice(src []uint32) []*uint32 {
dst := make([]*uint32, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Uint64 returns a pointer to the uint64 value passed in.
func Uint64(v uint64) *uint64 {
return &v
}
// Uint64Slice converts a slice of uint64 values into a slice of
// uint64 pointers
func Uint64Slice(src []uint64) []*uint64 {
dst := make([]*uint64, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Float32 returns a pointer to the float32 value passed in.
func Float32(v float32) *float32 {
return &v
}
// Float32Slice converts a slice of float32 values into a slice of
// float32 pointers
func Float32Slice(src []float32) []*float32 {
dst := make([]*float32, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Float64 returns a pointer to the float64 value passed in.
func Float64(v float64) *float64 {
return &v
}
// Float64Slice converts a slice of float64 values into a slice of
// float64 pointers
func Float64Slice(src []float64) []*float64 {
dst := make([]*float64, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Duration returns a pointer to the Duration value passed in.
func Duration(v time.Duration) *time.Duration {
return &v
}

View File

@ -0,0 +1,15 @@
package utils
import "io"
// File is the structure used to receive / send a file from / to the API
type File struct {
// Name of the file
Name string `json:"name"`
// ContentType used in the HTTP header `Content-Type`
ContentType string `json:"content_type"`
// Content of the file
Content io.Reader `json:"content"`
}

View File

@ -0,0 +1,155 @@
package utils
import (
"encoding/json"
"github.com/scaleway/scaleway-sdk-go/logger"
)
// Zone is an availability zone
type Zone string
const (
// ZoneFrPar1 represents the fr-par-1 zone
ZoneFrPar1 = Zone("fr-par-1")
// ZoneFrPar2 represents the fr-par-2 zone
ZoneFrPar2 = Zone("fr-par-2")
// ZoneNlAms1 represents the nl-ams-1 zone
ZoneNlAms1 = Zone("nl-ams-1")
)
var (
// AllZones is an array that list all zones
AllZones = []Zone{
ZoneFrPar1,
ZoneFrPar2,
ZoneNlAms1,
}
)
// Exists checks whether a zone exists
func (zone *Zone) Exists() bool {
for _, z := range AllZones {
if z == *zone {
return true
}
}
return false
}
// Region is a geographical location
type Region string
const (
// RegionFrPar represents the fr-par region
RegionFrPar = Region("fr-par")
// RegionNlAms represents the nl-ams region
RegionNlAms = Region("nl-ams")
)
var (
// AllRegions is an array that list all regions
AllRegions = []Region{
RegionFrPar,
RegionNlAms,
}
)
// Exists checks whether a region exists
func (region *Region) Exists() bool {
for _, r := range AllRegions {
if r == *region {
return true
}
}
return false
}
// GetZones is a function that returns the zones for the specified region
func (region Region) GetZones() []Zone {
switch region {
case RegionFrPar:
return []Zone{ZoneFrPar1, ZoneFrPar2}
case RegionNlAms:
return []Zone{ZoneNlAms1}
default:
return []Zone{}
}
}
// ParseZone parse a string value into a Zone object
func ParseZone(zone string) (Zone, error) {
switch zone {
case "par1":
// would be triggered by API market place
// logger.Warningf("par1 is a deprecated name for zone, use fr-par-1 instead")
return ZoneFrPar1, nil
case "ams1":
// would be triggered by API market place
// logger.Warningf("ams1 is a deprecated name for zone, use nl-ams-1 instead")
return ZoneNlAms1, nil
default:
newZone := Zone(zone)
if !newZone.Exists() {
logger.Warningf("%s is an unknown zone", newZone)
}
return newZone, nil
}
}
// UnmarshalJSON implements the Unmarshaler interface for a Zone.
// this to call ParseZone on the string input and return the correct Zone object.
func (zone *Zone) UnmarshalJSON(input []byte) error {
// parse input value as string
var stringValue string
err := json.Unmarshal(input, &stringValue)
if err != nil {
return err
}
// parse string as Zone
*zone, err = ParseZone(stringValue)
if err != nil {
return err
}
return nil
}
// ParseRegion parse a string value into a Zone object
func ParseRegion(region string) (Region, error) {
switch region {
case "par1":
// would be triggered by API market place
// logger.Warningf("par1 is a deprecated name for region, use fr-par instead")
return RegionFrPar, nil
case "ams1":
// would be triggered by API market place
// logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead")
return RegionNlAms, nil
default:
newRegion := Region(region)
if !newRegion.Exists() {
logger.Warningf("%s is an unknown region", newRegion)
}
return newRegion, nil
}
}
// UnmarshalJSON implements the Unmarshaler interface for a Region.
// this to call ParseRegion on the string input and return the correct Region object.
func (region *Region) UnmarshalJSON(input []byte) error {
// parse input value as string
var stringValue string
err := json.Unmarshal(input, &stringValue)
if err != nil {
return err
}
// parse string as Region
*region, err = ParseRegion(stringValue)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,33 @@
package utils
// Money represents an amount of money with its currency type.
type Money struct {
// CurrencyCode is the 3-letter currency code defined in ISO 4217.
CurrencyCode string `json:"currency_code,omitempty"`
// Units is the whole units of the amount.
// For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar.
Units int64 `json:"units,omitempty"`
// Nanos is the number of nano (10^-9) units of the amount.
// The value must be between -999,999,999 and +999,999,999 inclusive.
// If `units` is positive, `nanos` must be positive or zero.
// If `units` is zero, `nanos` can be positive, zero, or negative.
// If `units` is negative, `nanos` must be negative or zero.
// For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
Nanos int32 `json:"nanos,omitempty"`
}
// NewMoneyFromFloat conerts a float with currency to a Money object.
func NewMoneyFromFloat(value float64, currency string) *Money {
return &Money{
CurrencyCode: currency,
Units: int64(value),
Nanos: int32((value - float64(int64(value))) * 1000000000),
}
}
// ToFloat converts a Money object to a float.
func (m *Money) ToFloat() float64 {
return float64(m.Units) + float64(m.Nanos)/1000000000
}

View File

@ -0,0 +1,17 @@
package utils
// ServiceInfo contains API metadata
// These metadata are only here for debugging. Do not rely on these values
type ServiceInfo struct {
// Name is the name of the API
Name string `json:"name"`
// Description is a human readable description for the API
Description string `json:"description"`
// Version is the version of the API
Version string `json:"version"`
// DocumentationUrl is the a web url where the documentation of the API can be found
DocumentationUrl *string `json:"documentation_url"`
}

View File

@ -0,0 +1,159 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package blowfish
// getNextWord returns the next big-endian uint32 value from the byte slice
// at the given position in a circular manner, updating the position.
func getNextWord(b []byte, pos *int) uint32 {
var w uint32
j := *pos
for i := 0; i < 4; i++ {
w = w<<8 | uint32(b[j])
j++
if j >= len(b) {
j = 0
}
}
*pos = j
return w
}
// ExpandKey performs a key expansion on the given *Cipher. Specifically, it
// performs the Blowfish algorithm's key schedule which sets up the *Cipher's
// pi and substitution tables for calls to Encrypt. This is used, primarily,
// by the bcrypt package to reuse the Blowfish key schedule during its
// set up. It's unlikely that you need to use this directly.
func ExpandKey(key []byte, c *Cipher) {
j := 0
for i := 0; i < 18; i++ {
// Using inlined getNextWord for performance.
var d uint32
for k := 0; k < 4; k++ {
d = d<<8 | uint32(key[j])
j++
if j >= len(key) {
j = 0
}
}
c.p[i] ^= d
}
var l, r uint32
for i := 0; i < 18; i += 2 {
l, r = encryptBlock(l, r, c)
c.p[i], c.p[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l, r = encryptBlock(l, r, c)
c.s0[i], c.s0[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l, r = encryptBlock(l, r, c)
c.s1[i], c.s1[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l, r = encryptBlock(l, r, c)
c.s2[i], c.s2[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l, r = encryptBlock(l, r, c)
c.s3[i], c.s3[i+1] = l, r
}
}
// This is similar to ExpandKey, but folds the salt during the key
// schedule. While ExpandKey is essentially expandKeyWithSalt with an all-zero
// salt passed in, reusing ExpandKey turns out to be a place of inefficiency
// and specializing it here is useful.
func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) {
j := 0
for i := 0; i < 18; i++ {
c.p[i] ^= getNextWord(key, &j)
}
j = 0
var l, r uint32
for i := 0; i < 18; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.p[i], c.p[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s0[i], c.s0[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s1[i], c.s1[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s2[i], c.s2[i+1] = l, r
}
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s3[i], c.s3[i+1] = l, r
}
}
func encryptBlock(l, r uint32, c *Cipher) (uint32, uint32) {
xl, xr := l, r
xl ^= c.p[0]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[1]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[2]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[3]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[4]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[5]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[6]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[7]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[8]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[9]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[10]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[11]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[12]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[13]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[14]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[15]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[16]
xr ^= c.p[17]
return xr, xl
}
func decryptBlock(l, r uint32, c *Cipher) (uint32, uint32) {
xl, xr := l, r
xl ^= c.p[17]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[16]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[15]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[14]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[13]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[12]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[11]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[10]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[9]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[8]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[7]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[6]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[5]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[4]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[3]
xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[2]
xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[1]
xr ^= c.p[0]
return xr, xl
}

View File

@ -0,0 +1,91 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package blowfish implements Bruce Schneier's Blowfish encryption algorithm.
package blowfish // import "golang.org/x/crypto/blowfish"
// The code is a port of Bruce Schneier's C implementation.
// See https://www.schneier.com/blowfish.html.
import "strconv"
// The Blowfish block size in bytes.
const BlockSize = 8
// A Cipher is an instance of Blowfish encryption using a particular key.
type Cipher struct {
p [18]uint32
s0, s1, s2, s3 [256]uint32
}
type KeySizeError int
func (k KeySizeError) Error() string {
return "crypto/blowfish: invalid key size " + strconv.Itoa(int(k))
}
// NewCipher creates and returns a Cipher.
// The key argument should be the Blowfish key, from 1 to 56 bytes.
func NewCipher(key []byte) (*Cipher, error) {
var result Cipher
if k := len(key); k < 1 || k > 56 {
return nil, KeySizeError(k)
}
initCipher(&result)
ExpandKey(key, &result)
return &result, nil
}
// NewSaltedCipher creates a returns a Cipher that folds a salt into its key
// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is
// sufficient and desirable. For bcrypt compatibility, the key can be over 56
// bytes.
func NewSaltedCipher(key, salt []byte) (*Cipher, error) {
if len(salt) == 0 {
return NewCipher(key)
}
var result Cipher
if k := len(key); k < 1 {
return nil, KeySizeError(k)
}
initCipher(&result)
expandKeyWithSalt(key, salt, &result)
return &result, nil
}
// BlockSize returns the Blowfish block size, 8 bytes.
// It is necessary to satisfy the Block interface in the
// package "crypto/cipher".
func (c *Cipher) BlockSize() int { return BlockSize }
// Encrypt encrypts the 8-byte buffer src using the key k
// and stores the result in dst.
// Note that for amounts of data larger than a block,
// it is not safe to just call Encrypt on successive blocks;
// instead, use an encryption mode like CBC (see crypto/cipher/cbc.go).
func (c *Cipher) Encrypt(dst, src []byte) {
l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3])
r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7])
l, r = encryptBlock(l, r, c)
dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l)
dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r)
}
// Decrypt decrypts the 8-byte buffer src using the key k
// and stores the result in dst.
func (c *Cipher) Decrypt(dst, src []byte) {
l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3])
r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7])
l, r = decryptBlock(l, r, c)
dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l)
dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r)
}
func initCipher(c *Cipher) {
copy(c.p[0:], p[0:])
copy(c.s0[0:], s0[0:])
copy(c.s1[0:], s1[0:])
copy(c.s2[0:], s2[0:])
copy(c.s3[0:], s3[0:])
}

View File

@ -0,0 +1,199 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The startup permutation array and substitution boxes.
// They are the hexadecimal digits of PI; see:
// https://www.schneier.com/code/constants.txt.
package blowfish
var s0 = [256]uint32{
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96,
0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658,
0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e,
0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6,
0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c,
0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1,
0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a,
0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176,
0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706,
0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b,
0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c,
0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a,
0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760,
0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8,
0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33,
0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0,
0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777,
0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705,
0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e,
0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9,
0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f,
0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
}
var s1 = [256]uint32{
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d,
0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65,
0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9,
0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d,
0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc,
0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908,
0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124,
0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908,
0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b,
0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa,
0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d,
0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5,
0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96,
0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca,
0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77,
0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054,
0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea,
0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646,
0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea,
0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e,
0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd,
0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
}
var s2 = [256]uint32{
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7,
0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af,
0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4,
0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec,
0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332,
0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58,
0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22,
0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60,
0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99,
0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74,
0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3,
0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979,
0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa,
0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086,
0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24,
0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84,
0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09,
0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe,
0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0,
0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188,
0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8,
0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
}
var s3 = [256]uint32{
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742,
0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79,
0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a,
0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1,
0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797,
0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6,
0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba,
0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5,
0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce,
0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd,
0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb,
0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc,
0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc,
0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a,
0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a,
0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b,
0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e,
0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623,
0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a,
0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3,
0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c,
0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6,
}
var p = [18]uint32{
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0,
0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b,
}