Migrate to official OCI Go SDK

This commit is contained in:
Harvey Lowndes 2018-04-11 10:20:40 +01:00
parent 31973d9f8b
commit c442ba165e
29 changed files with 433 additions and 2092 deletions

View File

@ -3,12 +3,12 @@ package oci
import (
"fmt"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
"github.com/oracle/oci-go-sdk/core"
)
// Artifact is an artifact implementation that contains a built Custom Image.
type Artifact struct {
Image client.Image
Image core.Image
Region string
driver Driver
}
@ -26,13 +26,18 @@ func (a *Artifact) Files() []string {
// Id returns the OCID of the associated Image.
func (a *Artifact) Id() string {
return a.Image.ID
return *a.Image.Id
}
func (a *Artifact) String() string {
var displayName string
if a.Image.DisplayName != nil {
displayName = *a.Image.DisplayName
}
return fmt.Sprintf(
"An image was created: '%v' (OCID: %v) in region '%v'",
a.Image.DisplayName, a.Image.ID, a.Region,
displayName, *a.Image.Id, a.Region,
)
}
@ -42,5 +47,5 @@ func (a *Artifact) State(name string) interface{} {
// Destroy deletes the custom image associated with the artifact.
func (a *Artifact) Destroy() error {
return a.driver.DeleteImage(a.Image.ID)
return a.driver.DeleteImage(*a.Image.Id)
}

View File

@ -7,11 +7,11 @@ import (
"log"
ocommon "github.com/hashicorp/packer/builder/oracle/common"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/oracle/oci-go-sdk/core"
)
// BuilderId uniquely identifies the builder
@ -78,10 +78,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, rawErr.(error)
}
region, err := b.config.ConfigProvider.Region()
if err != nil {
return nil, err
}
// Build the artifact and return it
artifact := &Artifact{
Image: state.Get("image").(client.Image),
Region: b.config.AccessCfg.Region,
Image: state.Get("image").(core.Image),
Region: region,
driver: driver,
}

View File

@ -1,216 +0,0 @@
package oci
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
)
const (
contentType = "Content-Type"
jsonContentType = "application/json"
)
// baseClient provides a basic (AND INTENTIONALLY INCOMPLETE) JSON REST client
// that abstracts away some of the repetitive code required in the OCI Client.
type baseClient struct {
httpClient *http.Client
method string
url string
queryStruct interface{}
header http.Header
body interface{}
}
// newBaseClient constructs a default baseClient.
func newBaseClient() *baseClient {
return &baseClient{
httpClient: http.DefaultClient,
method: "GET",
header: make(http.Header),
}
}
// New creates a copy of an existing baseClient.
func (c *baseClient) New() *baseClient {
// Copy headers
header := make(http.Header)
for k, v := range c.header {
header[k] = v
}
return &baseClient{
httpClient: c.httpClient,
method: c.method,
url: c.url,
header: header,
}
}
// Client sets the http Client used to perform requests.
func (c *baseClient) Client(httpClient *http.Client) *baseClient {
if httpClient == nil {
c.httpClient = http.DefaultClient
} else {
c.httpClient = httpClient
}
return c
}
// Base sets the base client url.
func (c *baseClient) Base(path string) *baseClient {
c.url = path
return c
}
// Path extends the client url.
func (c *baseClient) Path(path string) *baseClient {
baseURL, baseErr := url.Parse(c.url)
pathURL, pathErr := url.Parse(path)
// Bail on parsing error leaving the client's url unmodified
if baseErr != nil || pathErr != nil {
return c
}
c.url = baseURL.ResolveReference(pathURL).String()
return c
}
// QueryStruct sets the struct from which the request querystring is built.
func (c *baseClient) QueryStruct(params interface{}) *baseClient {
c.queryStruct = params
return c
}
// SetBody wraps a given struct for serialisation and sets the client body.
func (c *baseClient) SetBody(params interface{}) *baseClient {
c.body = params
return c
}
// Header
// AddHeader adds a HTTP header to the client. Existing keys will be extended.
func (c *baseClient) AddHeader(key, value string) *baseClient {
c.header.Add(key, value)
return c
}
// SetHeader sets a HTTP header on the client. Existing keys will be
// overwritten.
func (c *baseClient) SetHeader(key, value string) *baseClient {
c.header.Add(key, value)
return c
}
// HTTP methods (subset)
// Get sets the client's HTTP method to GET.
func (c *baseClient) Get(path string) *baseClient {
c.method = "GET"
return c.Path(path)
}
// Post sets the client's HTTP method to POST.
func (c *baseClient) Post(path string) *baseClient {
c.method = "POST"
return c.Path(path)
}
// Delete sets the client's HTTP method to DELETE.
func (c *baseClient) Delete(path string) *baseClient {
c.method = "DELETE"
return c.Path(path)
}
// Do executes a HTTP request and returns the response encoded as either error
// or success values.
func (c *baseClient) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
if successV != nil {
err = json.NewDecoder(resp.Body).Decode(successV)
}
} else {
if failureV != nil {
err = json.NewDecoder(resp.Body).Decode(failureV)
}
}
return resp, err
}
// Request builds a http.Request from the baseClient instance.
func (c *baseClient) Request() (*http.Request, error) {
reqURL, err := url.Parse(c.url)
if err != nil {
return nil, err
}
if c.queryStruct != nil {
err = addQueryStruct(reqURL, c.queryStruct)
if err != nil {
return nil, err
}
}
body := &bytes.Buffer{}
if c.body != nil {
if err := json.NewEncoder(body).Encode(c.body); err != nil {
return nil, err
}
}
req, err := http.NewRequest(c.method, reqURL.String(), body)
if err != nil {
return nil, err
}
// Add headers to request
for k, vs := range c.header {
for _, v := range vs {
req.Header.Add(k, v)
}
}
return req, nil
}
// Receive creates a http request from the client and executes it returning the
// response.
func (c *baseClient) Receive(successV, failureV interface{}) (*http.Response, error) {
req, err := c.Request()
if err != nil {
return nil, err
}
return c.Do(req, successV, failureV)
}
// addQueryStruct converts a struct to a querystring and merges any values
// provided in the URL itself.
func addQueryStruct(reqURL *url.URL, queryStruct interface{}) error {
urlValues, err := url.ParseQuery(reqURL.RawQuery)
if err != nil {
return err
}
queryValues, err := query.Values(queryStruct)
if err != nil {
return err
}
for k, vs := range queryValues {
for _, v := range vs {
urlValues.Add(k, v)
}
}
reqURL.RawQuery = urlValues.Encode()
return nil
}

View File

@ -1,31 +0,0 @@
package oci
import (
"net/http"
)
const (
apiVersion = "20160918"
userAgent = "go-oci/" + apiVersion
baseURLPattern = "https://%s.%s.oraclecloud.com/%s/"
)
// Client is the main interface through which consumers interact with the OCI
// API.
type Client struct {
UserAgent string
Compute *ComputeClient
Config *Config
}
// NewClient creates a new Client for communicating with the OCI API.
func NewClient(config *Config) (*Client, error) {
transport := NewTransport(http.DefaultTransport, config)
base := newBaseClient().Client(&http.Client{Transport: transport})
return &Client{
UserAgent: userAgent,
Compute: NewComputeClient(base.New().Base(config.getBaseURL("iaas"))),
Config: config,
}, nil
}

View File

@ -1,49 +0,0 @@
package oci
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"github.com/go-ini/ini"
)
var (
mux *http.ServeMux
client *Client
server *httptest.Server
keyFile *os.File
)
// setup sets up a test HTTP server along with a oci.Client that is
// configured to talk to that test server. Tests should register handlers on
// mux which provide mock responses for the API method being tested.
func setup() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
parsedURL, _ := url.Parse(server.URL)
config := &Config{}
config.baseURL = parsedURL.String()
var cfg *ini.File
var err error
cfg, keyFile, err = BaseTestConfig()
config, err = loadConfigSection(cfg, "DEFAULT", config)
if err != nil {
panic(err)
}
client, err = NewClient(config)
if err != nil {
panic("Failed to instantiate test client")
}
}
// teardown closes the test HTTP server
func teardown() {
server.Close()
os.Remove(keyFile.Name())
}

View File

@ -1,21 +0,0 @@
package oci
// ComputeClient is a client for the OCI Compute API.
type ComputeClient struct {
BaseURL string
Instances *InstanceService
Images *ImageService
VNICAttachments *VNICAttachmentService
VNICs *VNICService
}
// NewComputeClient creates a new client for communicating with the OCI
// Compute API.
func NewComputeClient(s *baseClient) *ComputeClient {
return &ComputeClient{
Instances: NewInstanceService(s),
Images: NewImageService(s),
VNICAttachments: NewVNICAttachmentService(s),
VNICs: NewVNICService(s),
}
}

View File

@ -1,240 +0,0 @@
package oci
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/go-ini/ini"
"github.com/mitchellh/go-homedir"
)
// Config API authentication and target configuration
type Config struct {
// User OCID e.g. ocid1.user.oc1..aaaaaaaadcshyehbkvxl7arse3lv7z5oknexjgfhnhwidtugsxhlm4247
User string `ini:"user"`
// User's Tenancy OCID e.g. ocid1.tenancy.oc1..aaaaaaaagtgvshv6opxzjyzkupkt64ymd32n6kbomadanpcg43d
Tenancy string `ini:"tenancy"`
// Bare metal region identifier (e.g. us-phoenix-1)
Region string `ini:"region"`
// Hex key fingerprint (e.g. b5:a0:62:57:28:0d:fd:c9:59:16:eb:d4:51:9f:70:e4)
Fingerprint string `ini:"fingerprint"`
// Path to OCI config file (e.g. ~/.oci/config)
KeyFile string `ini:"key_file"`
// Passphrase used for the key, if it is encrypted.
PassPhrase string `ini:"pass_phrase"`
// Private key (loaded via LoadPrivateKey or ParsePrivateKey)
Key *rsa.PrivateKey
// Used to override base API URL.
baseURL string
}
// getBaseURL returns either the specified base URL or builds the appropriate
// URL based on service, region, and API version.
func (c *Config) getBaseURL(service string) string {
if c.baseURL != "" {
return c.baseURL
}
return fmt.Sprintf(baseURLPattern, service, c.Region, apiVersion)
}
// LoadConfigsFromFile loads all oracle oci configurations from a file
// (generally ~/.oci/config).
func LoadConfigsFromFile(path string) (map[string]*Config, error) {
if _, err := os.Stat(path); err != nil {
return nil, fmt.Errorf("Oracle OCI config file is missing: %s", path)
}
cfgFile, err := ini.Load(path)
if err != nil {
err := fmt.Errorf("Failed to parse config file %s: %s", path, err.Error())
return nil, err
}
configs := make(map[string]*Config)
// Load DEFAULT section to populate defaults for all other configs
config, err := loadConfigSection(cfgFile, "DEFAULT", nil)
if err != nil {
return nil, err
}
configs["DEFAULT"] = config
// Load other sections.
for _, sectionName := range cfgFile.SectionStrings() {
if sectionName == "DEFAULT" {
continue
}
// Map to Config struct with defaults from DEFAULT section.
config, err := loadConfigSection(cfgFile, sectionName, configs["DEFAULT"])
if err != nil {
return nil, err
}
configs[sectionName] = config
}
return configs, nil
}
// Loads an individual Config object from a ini.Section in the Oracle OCI config
// file.
func loadConfigSection(f *ini.File, sectionName string, config *Config) (*Config, error) {
if config == nil {
config = &Config{}
}
section, err := f.GetSection(sectionName)
if err != nil {
return nil, fmt.Errorf("Config file does not contain a %s section", sectionName)
}
if err := section.MapTo(config); err != nil {
return nil, err
}
config.Key, err = LoadPrivateKey(config)
if err != nil {
return nil, err
}
return config, err
}
// LoadPrivateKey loads private key from disk and parses it.
func LoadPrivateKey(config *Config) (*rsa.PrivateKey, error) {
// Expand '~' to $HOME
path, err := homedir.Expand(config.KeyFile)
if err != nil {
return nil, err
}
// Read and parse API signing key
keyContent, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
key, err := ParsePrivateKey(keyContent, []byte(config.PassPhrase))
return key, err
}
// ParsePrivateKey parses a PEM encoded array of bytes into an rsa.PrivateKey.
// Attempts to decrypt the PEM encoded array of bytes with the given password
// if the PEM encoded byte array is encrypted.
func ParsePrivateKey(content, password []byte) (*rsa.PrivateKey, error) {
keyBlock, _ := pem.Decode(content)
if keyBlock == nil {
return nil, errors.New("could not decode PEM private key")
}
var der []byte
var err error
if x509.IsEncryptedPEMBlock(keyBlock) {
if len(password) < 1 {
return nil, errors.New("encrypted private key but no pass phrase provided")
}
der, err = x509.DecryptPEMBlock(keyBlock, password)
if err != nil {
return nil, err
}
} else {
der = keyBlock.Bytes
}
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
key, err := x509.ParsePKCS8PrivateKey(der)
if err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
default:
return nil, errors.New("Private key is not an RSA private key")
}
}
return nil, fmt.Errorf("Failed to parse private key :%s", err)
}
// BaseTestConfig creates the base (DEFAULT) config including a temporary key
// file.
// NOTE: Caller is responsible for removing temporary key file.
func BaseTestConfig() (*ini.File, *os.File, error) {
keyFile, err := generateRSAKeyFile()
if err != nil {
return nil, keyFile, err
}
// Build ini
cfg := ini.Empty()
section, _ := cfg.NewSection("DEFAULT")
section.NewKey("region", "us-ashburn-1")
section.NewKey("tenancy", "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
section.NewKey("user", "ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
section.NewKey("fingerprint", "3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55")
section.NewKey("key_file", keyFile.Name())
return cfg, keyFile, nil
}
// WriteTestConfig writes a ini.File to a temporary file for use in unit tests.
// NOTE: Caller is responsible for removing temporary file.
func WriteTestConfig(cfg *ini.File) (*os.File, error) {
confFile, err := ioutil.TempFile("", "config_file")
if err != nil {
return nil, err
}
_, err = cfg.WriteTo(confFile)
if err != nil {
os.Remove(confFile.Name())
return nil, err
}
return confFile, nil
}
// generateRSAKeyFile generates an RSA key file for use in unit tests.
// NOTE: The caller is responsible for deleting the temporary file.
func generateRSAKeyFile() (*os.File, error) {
// Create temporary file for the key
f, err := ioutil.TempFile("", "key")
if err != nil {
return nil, err
}
// Generate key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
return nil, err
}
// ASN.1 DER encoded form
privDer := x509.MarshalPKCS1PrivateKey(priv)
privBlk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privDer,
}
// Write the key out
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
return nil, err
}
return f, nil
}

View File

@ -1,283 +0,0 @@
package oci
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"os"
"reflect"
"strings"
"testing"
)
func TestNewConfigMissingFile(t *testing.T) {
// WHEN
_, err := LoadConfigsFromFile("some/invalid/path")
// THEN
if err == nil {
t.Error("Expected missing file error")
}
}
func TestNewConfigDefaultOnly(t *testing.T) {
// GIVEN
// Get DEFAULT config
cfg, keyFile, err := BaseTestConfig()
defer os.Remove(keyFile.Name())
// Write test config to file
f, err := WriteTestConfig(cfg)
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
// WHEN
// Load configs
cfgs, err := LoadConfigsFromFile(f.Name())
if err != nil {
t.Fatal(err)
}
// THEN
if _, ok := cfgs["DEFAULT"]; !ok {
t.Fatal("Expected DEFAULT config to exist in map")
}
}
func TestNewConfigDefaultsPopulated(t *testing.T) {
// GIVEN
// Get DEFAULT config
cfg, keyFile, err := BaseTestConfig()
defer os.Remove(keyFile.Name())
admin := cfg.Section("ADMIN")
admin.NewKey("user", "ocid1.user.oc1..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
admin.NewKey("fingerprint", "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11")
// Write test config to file
f, err := WriteTestConfig(cfg)
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
// WHEN
cfgs, err := LoadConfigsFromFile(f.Name())
adminConfig, ok := cfgs["ADMIN"]
// THEN
if !ok {
t.Fatal("Expected ADMIN config to exist in map")
}
if adminConfig.Region != "us-ashburn-1" {
t.Errorf("Expected 'us-ashburn-1', got '%s'", adminConfig.Region)
}
}
func TestNewConfigDefaultsOverridden(t *testing.T) {
// GIVEN
// Get DEFAULT config
cfg, keyFile, err := BaseTestConfig()
defer os.Remove(keyFile.Name())
admin := cfg.Section("ADMIN")
admin.NewKey("user", "ocid1.user.oc1..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
admin.NewKey("fingerprint", "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11")
// Write test config to file
f, err := WriteTestConfig(cfg)
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name()) // clean up
// WHEN
cfgs, err := LoadConfigsFromFile(f.Name())
adminConfig, ok := cfgs["ADMIN"]
// THEN
if !ok {
t.Fatal("Expected ADMIN config to exist in map")
}
if adminConfig.Fingerprint != "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11" {
t.Errorf("Expected fingerprint '11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11', got '%s'",
adminConfig.Fingerprint)
}
}
func TestParseEncryptedPrivateKeyValidPassword(t *testing.T) {
// Generate private key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
t.Fatalf("Unexpected generating RSA key: %+v", err)
}
publicKey := priv.PublicKey
// ASN.1 DER encoded form
privDer := x509.MarshalPKCS1PrivateKey(priv)
blockType := "RSA PRIVATE KEY"
password := []byte("password")
cipherType := x509.PEMCipherAES256
// Encrypt priv with password
encryptedPEMBlock, err := x509.EncryptPEMBlock(
rand.Reader,
blockType,
privDer,
password,
cipherType)
if err != nil {
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
}
// Parse private key
key, err := ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), password)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}
// Check we get the same key back
if !reflect.DeepEqual(publicKey, key.PublicKey) {
t.Errorf("expected public key of encrypted and decrypted key to match")
}
}
func TestParseEncryptedPrivateKeyPKCS8(t *testing.T) {
// Generate private key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
t.Fatalf("Unexpected generating RSA key: %+v", err)
}
publicKey := priv.PublicKey
// Implements x509.MarshalPKCS8PrivateKey which is not included in the
// standard library.
pkey := struct {
Version int
PrivateKeyAlgorithm []asn1.ObjectIdentifier
PrivateKey []byte
}{
Version: 0,
PrivateKeyAlgorithm: []asn1.ObjectIdentifier{{1, 2, 840, 113549, 1, 1, 1}},
PrivateKey: x509.MarshalPKCS1PrivateKey(priv),
}
privDer, err := asn1.Marshal(pkey)
if err != nil {
t.Fatalf("Unexpected marshaling RSA key: %+v", err)
}
blockType := "RSA PRIVATE KEY"
password := []byte("password")
cipherType := x509.PEMCipherAES256
// Encrypt priv with password
encryptedPEMBlock, err := x509.EncryptPEMBlock(
rand.Reader,
blockType,
privDer,
password,
cipherType)
if err != nil {
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
}
// Parse private key
key, err := ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), password)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}
// Check we get the same key back
if !reflect.DeepEqual(publicKey, key.PublicKey) {
t.Errorf("expected public key of encrypted and decrypted key to match")
}
}
func TestParseEncryptedPrivateKeyInvalidPassword(t *testing.T) {
// Generate private key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
t.Fatalf("Unexpected generating RSA key: %+v", err)
}
// ASN.1 DER encoded form
privDer := x509.MarshalPKCS1PrivateKey(priv)
blockType := "RSA PRIVATE KEY"
password := []byte("password")
cipherType := x509.PEMCipherAES256
// Encrypt priv with password
encryptedPEMBlock, err := x509.EncryptPEMBlock(
rand.Reader,
blockType,
privDer,
password,
cipherType)
if err != nil {
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
}
// Parse private key (with wrong password)
_, err = ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), []byte("foo"))
if err == nil {
t.Fatalf("Expected error, got nil")
}
if !strings.Contains(err.Error(), "decryption password incorrect") {
t.Errorf("Expected error to contain 'decryption password incorrect', got %+v", err)
}
}
func TestParseEncryptedPrivateKeyInvalidNoPassword(t *testing.T) {
// Generate private key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
t.Fatalf("Unexpected generating RSA key: %+v", err)
}
// ASN.1 DER encoded form
privDer := x509.MarshalPKCS1PrivateKey(priv)
blockType := "RSA PRIVATE KEY"
password := []byte("password")
cipherType := x509.PEMCipherAES256
// Encrypt priv with password
encryptedPEMBlock, err := x509.EncryptPEMBlock(
rand.Reader,
blockType,
privDer,
password,
cipherType)
if err != nil {
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
}
// Parse private key (with wrong password)
_, err = ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), []byte{})
if err == nil {
t.Fatalf("Expected error, got nil")
}
if !strings.Contains(err.Error(), "no pass phrase provided") {
t.Errorf("Expected error to contain 'no pass phrase provided', got %+v", err)
}
}

View File

@ -1,27 +0,0 @@
package oci
import "fmt"
// APIError encapsulates an error returned from the API
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (e APIError) Error() string {
return fmt.Sprintf("OCI: [%s] '%s'", e.Code, e.Message)
}
// firstError is a helper function to work out which error to return from calls
// to the API.
func firstError(err error, apiError *APIError) error {
if err != nil {
return err
}
if apiError != nil && len(apiError.Code) > 0 {
return apiError
}
return nil
}

View File

@ -1,122 +0,0 @@
package oci
import (
"time"
)
// ImageService enables communicating with the OCI compute API's instance
// related endpoints.
type ImageService struct {
client *baseClient
}
// NewImageService creates a new ImageService for communicating with the
// OCI compute API's instance related endpoints.
func NewImageService(s *baseClient) *ImageService {
return &ImageService{
client: s.New().Path("images/"),
}
}
// Image details a OCI boot disk image.
type Image struct {
// The OCID of the image originally used to launch the instance.
BaseImageID string `json:"baseImageId,omitempty"`
// The OCID of the compartment containing the instance you want to use
// as the basis for the image.
CompartmentID string `json:"compartmentId"`
// Whether instances launched with this image can be used to create new
// images.
CreateImageAllowed bool `json:"createImageAllowed"`
// A user-friendly name for the image. It does not have to be unique,
// and it's changeable. You cannot use an Oracle-provided image name
// as a custom image name.
DisplayName string `json:"displayName,omitempty"`
// The OCID of the image.
ID string `json:"id"`
// Current state of the image. Allowed values are:
// - PROVISIONING
// - AVAILABLE
// - DISABLED
// - DELETED
LifecycleState string `json:"lifecycleState"`
// The image's operating system (e.g. Oracle Linux).
OperatingSystem string `json:"operatingSystem"`
// The image's operating system version (e.g. 7.2).
OperatingSystemVersion string `json:"operatingSystemVersion"`
// The date and time the image was created.
TimeCreated time.Time `json:"timeCreated"`
}
// GetImageParams are the parameters available when communicating with the
// GetImage API endpoint.
type GetImageParams struct {
ID string `url:"imageId"`
}
// Get returns a single Image
func (s *ImageService) Get(params *GetImageParams) (Image, error) {
image := Image{}
e := &APIError{}
_, err := s.client.New().Get(params.ID).Receive(&image, e)
err = firstError(err, e)
return image, err
}
// CreateImageParams are the parameters available when communicating with
// the CreateImage API endpoint.
type CreateImageParams struct {
CompartmentID string `json:"compartmentId"`
DisplayName string `json:"displayName,omitempty"`
InstanceID string `json:"instanceId"`
}
// Create creates a new custom image based on a running compute instance. It
// does *not* wait for the imaging process to finish.
func (s *ImageService) Create(params *CreateImageParams) (Image, error) {
image := Image{}
e := &APIError{}
_, err := s.client.New().Post("").SetBody(params).Receive(&image, &e)
err = firstError(err, e)
return image, err
}
// GetResourceState GETs the LifecycleState of the given image id.
func (s *ImageService) GetResourceState(id string) (string, error) {
image, err := s.Get(&GetImageParams{ID: id})
if err != nil {
return "", err
}
return image.LifecycleState, nil
}
// DeleteImageParams are the parameters available when communicating with
// the DeleteImage API endpoint.
type DeleteImageParams struct {
ID string `url:"imageId"`
}
// Delete deletes an existing custom image.
// NOTE: Deleting an image results in the API endpoint returning 404 on
// subsequent calls. As such deletion can't be waited on with a Waiter.
func (s *ImageService) Delete(params *DeleteImageParams) error {
e := &APIError{}
_, err := s.client.New().Delete(params.ID).SetBody(params).Receive(nil, e)
err = firstError(err, e)
return err
}

View File

@ -1,115 +0,0 @@
package oci
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestGetImage(t *testing.T) {
setup()
defer teardown()
id := "ocid1.image.oc1.phx.a"
path := fmt.Sprintf("/images/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"id":"%s"}`, id)
})
image, err := client.Compute.Images.Get(&GetImageParams{ID: id})
if err != nil {
t.Errorf("Client.Compute.Images.Get() returned error: %v", err)
}
want := Image{ID: id}
if !reflect.DeepEqual(image, want) {
t.Errorf("Client.Compute.Images.Get() returned %+v, want %+v", image, want)
}
}
func TestCreateImage(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/images/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"displayName": "go-oci test"}`)
})
params := &CreateImageParams{
CompartmentID: "ocid1.compartment.oc1..a",
DisplayName: "go-oci test image",
InstanceID: "ocid1.image.oc1.phx.a",
}
image, err := client.Compute.Images.Create(params)
if err != nil {
t.Errorf("Client.Compute.Images.Create() returned error: %v", err)
}
want := Image{DisplayName: "go-oci test"}
if !reflect.DeepEqual(image, want) {
t.Errorf("Client.Compute.Images.Create() returned %+v, want %+v", image, want)
}
}
func TestImageGetResourceState(t *testing.T) {
setup()
defer teardown()
id := "ocid1.image.oc1.phx.a"
path := fmt.Sprintf("/images/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"LifecycleState": "AVAILABLE"}`)
})
state, err := client.Compute.Images.GetResourceState(id)
if err != nil {
t.Errorf("Client.Compute.Images.GetResourceState() returned error: %v", err)
}
want := "AVAILABLE"
if state != want {
t.Errorf("Client.Compute.Images.GetResourceState() returned %+v, want %+v", state, want)
}
}
func TestImageGetResourceStateInvalidID(t *testing.T) {
setup()
defer teardown()
id := "ocid1.image.oc1.phx.a"
path := fmt.Sprintf("/images/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"code": "NotAuthorizedOrNotFound"}`)
})
state, err := client.Compute.Images.GetResourceState(id)
if err == nil {
t.Errorf("Client.Compute.Images.GetResourceState() expected error, got %v", state)
}
want := &APIError{Code: "NotAuthorizedOrNotFound"}
if !reflect.DeepEqual(err, want) {
t.Errorf("Client.Compute.Images.GetResourceState() errored with %+v, want %+v", err, want)
}
}
func TestDeleteInstance(t *testing.T) {
setup()
defer teardown()
id := "ocid1.image.oc1.phx.a"
path := fmt.Sprintf("/images/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
err := client.Compute.Images.Delete(&DeleteImageParams{ID: id})
if err != nil {
t.Errorf("Client.Compute.Images.Delete() returned error: %v", err)
}
}

View File

@ -1,129 +0,0 @@
package oci
import (
"time"
)
// InstanceService enables communicating with the OCI compute API's instance
// related endpoints.
type InstanceService struct {
client *baseClient
}
// NewInstanceService creates a new InstanceService for communicating with the
// OCI compute API's instance related endpoints.
func NewInstanceService(s *baseClient) *InstanceService {
return &InstanceService{
client: s.New().Path("instances/"),
}
}
// Instance details a OCI compute instance.
type Instance struct {
// The Availability Domain the instance is running in.
AvailabilityDomain string `json:"availabilityDomain"`
// The OCID of the compartment that contains the instance.
CompartmentID string `json:"compartmentId"`
// A user-friendly name. Does not have to be unique, and it's changeable.
DisplayName string `json:"displayName,omitempty"`
// The OCID of the instance.
ID string `json:"id"`
// The image used to boot the instance.
ImageID string `json:"imageId,omitempty"`
// The current state of the instance. Allowed values:
// - PROVISIONING
// - RUNNING
// - STARTING
// - STOPPING
// - STOPPED
// - CREATING_IMAGE
// - TERMINATING
// - TERMINATED
LifecycleState string `json:"lifecycleState"`
// Custom metadata that you provide.
Metadata map[string]string `json:"metadata,omitempty"`
// The region that contains the Availability Domain the instance is running in.
Region string `json:"region"`
// The shape of the instance. The shape determines the number of CPUs
// and the amount of memory allocated to the instance.
Shape string `json:"shape"`
// The date and time the instance was created.
TimeCreated time.Time `json:"timeCreated"`
}
// GetInstanceParams are the parameters available when communicating with the
// GetInstance API endpoint.
type GetInstanceParams struct {
ID string `url:"instanceId,omitempty"`
}
// Get returns a single Instance
func (s *InstanceService) Get(params *GetInstanceParams) (Instance, error) {
instance := Instance{}
e := &APIError{}
_, err := s.client.New().Get(params.ID).Receive(&instance, e)
err = firstError(err, e)
return instance, err
}
// LaunchInstanceParams are the parameters available when communicating with
// the LunchInstance API endpoint.
type LaunchInstanceParams struct {
AvailabilityDomain string `json:"availabilityDomain,omitempty"`
CompartmentID string `json:"compartmentId,omitempty"`
DisplayName string `json:"displayName,omitempty"`
ImageID string `json:"imageId,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
OPCiPXEScript string `json:"opcIpxeScript,omitempty"`
Shape string `json:"shape,omitempty"`
SubnetID string `json:"subnetId,omitempty"`
}
// Launch creates a new OCI compute instance. It does *not* wait for the
// instance to boot.
func (s *InstanceService) Launch(params *LaunchInstanceParams) (Instance, error) {
instance := &Instance{}
e := &APIError{}
_, err := s.client.New().Post("").SetBody(params).Receive(instance, e)
err = firstError(err, e)
return *instance, err
}
// TerminateInstanceParams are the parameters available when communicating with
// the TerminateInstance API endpoint.
type TerminateInstanceParams struct {
ID string `url:"instanceId,omitempty"`
}
// Terminate terminates a running OCI compute instance.
// instance to boot.
func (s *InstanceService) Terminate(params *TerminateInstanceParams) error {
e := &APIError{}
_, err := s.client.New().Delete(params.ID).SetBody(params).Receive(nil, e)
err = firstError(err, e)
return err
}
// GetResourceState GETs the LifecycleState of the given instance id.
func (s *InstanceService) GetResourceState(id string) (string, error) {
instance, err := s.Get(&GetInstanceParams{ID: id})
if err != nil {
return "", err
}
return instance.LifecycleState, nil
}

View File

@ -1,96 +0,0 @@
package oci
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestGetInstance(t *testing.T) {
setup()
defer teardown()
id := "ocid1.instance.oc1.phx.a"
path := fmt.Sprintf("/instances/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"id":"%s"}`, id)
})
instance, err := client.Compute.Instances.Get(&GetInstanceParams{ID: id})
if err != nil {
t.Errorf("Client.Compute.Instances.Get() returned error: %v", err)
}
want := Instance{ID: id}
if !reflect.DeepEqual(instance, want) {
t.Errorf("Client.Compute.Instances.Get() returned %+v, want %+v", instance, want)
}
}
func TestLaunchInstance(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/instances/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"displayName": "go-oci test"}`)
})
params := &LaunchInstanceParams{
AvailabilityDomain: "aaaa:PHX-AD-1",
CompartmentID: "ocid1.compartment.oc1..a",
DisplayName: "go-oci test",
ImageID: "ocid1.image.oc1.phx.a",
Shape: "VM.Standard1.1",
SubnetID: "ocid1.subnet.oc1.phx.a",
}
instance, err := client.Compute.Instances.Launch(params)
if err != nil {
t.Errorf("Client.Compute.Instances.Launch() returned error: %v", err)
}
want := Instance{DisplayName: "go-oci test"}
if !reflect.DeepEqual(instance, want) {
t.Errorf("Client.Compute.Instances.Launch() returned %+v, want %+v", instance, want)
}
}
func TestTerminateInstance(t *testing.T) {
setup()
defer teardown()
id := "ocid1.instance.oc1.phx.a"
path := fmt.Sprintf("/instances/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
err := client.Compute.Instances.Terminate(&TerminateInstanceParams{ID: id})
if err != nil {
t.Errorf("Client.Compute.Instances.Terminate() returned error: %v", err)
}
}
func TestInstanceGetResourceState(t *testing.T) {
setup()
defer teardown()
id := "ocid1.instance.oc1.phx.a"
path := fmt.Sprintf("/instances/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"LifecycleState": "RUNNING"}`)
})
state, err := client.Compute.Instances.GetResourceState(id)
if err != nil {
t.Errorf("Client.Compute.Instances.GetResourceState() returned error: %v", err)
}
want := "RUNNING"
if state != want {
t.Errorf("Client.Compute.Instances.GetResourceState() returned %+v, want %+v", state, want)
}
}

View File

@ -1,116 +0,0 @@
package oci
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error {
return nil
}
// Transport adds OCI signature authentication to each outgoing request.
type Transport struct {
transport http.RoundTripper
config *Config
}
// NewTransport creates a new Transport to add OCI signature authentication
// to each outgoing request.
func NewTransport(transport http.RoundTripper, config *Config) *Transport {
return &Transport{
transport: transport,
config: config,
}
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
var buf *bytes.Buffer
if req.Body != nil {
buf = new(bytes.Buffer)
buf.ReadFrom(req.Body)
req.Body = nopCloser{buf}
}
if req.Header.Get("date") == "" {
req.Header.Set("date", time.Now().UTC().Format(http.TimeFormat))
}
if req.Header.Get("content-type") == "" {
req.Header.Set("content-type", "application/json")
}
if req.Header.Get("accept") == "" {
req.Header.Set("accept", "application/json")
}
if req.Header.Get("host") == "" {
req.Header.Set("host", req.URL.Host)
}
var signheaders []string
if (req.Method == "PUT" || req.Method == "POST") && buf != nil {
signheaders = []string{"(request-target)", "host", "date",
"content-length", "content-type", "x-content-sha256"}
if req.Header.Get("content-length") == "" {
req.Header.Set("content-length", strconv.Itoa(buf.Len()))
}
hasher := sha256.New()
hasher.Write(buf.Bytes())
hash := hasher.Sum(nil)
req.Header.Set("x-content-sha256", base64.StdEncoding.EncodeToString(hash))
} else {
signheaders = []string{"date", "host", "(request-target)"}
}
var signbuffer bytes.Buffer
for idx, header := range signheaders {
signbuffer.WriteString(header)
signbuffer.WriteString(": ")
if header == "(request-target)" {
signbuffer.WriteString(strings.ToLower(req.Method))
signbuffer.WriteString(" ")
signbuffer.WriteString(req.URL.RequestURI())
} else {
signbuffer.WriteString(req.Header.Get(header))
}
if idx < len(signheaders)-1 {
signbuffer.WriteString("\n")
}
}
h := sha256.New()
h.Write(signbuffer.Bytes())
digest := h.Sum(nil)
signature, err := rsa.SignPKCS1v15(rand.Reader, t.config.Key, crypto.SHA256, digest)
if err != nil {
return nil, err
}
authHeader := fmt.Sprintf("Signature headers=\"%s\","+
"keyId=\"%s/%s/%s\","+
"algorithm=\"rsa-sha256\","+
"signature=\"%s\","+
"version=\"1\"",
strings.Join(signheaders, " "),
t.config.Tenancy, t.config.User, t.config.Fingerprint,
base64.StdEncoding.EncodeToString(signature))
req.Header.Add("Authorization", authHeader)
return t.transport.RoundTrip(req)
}

View File

@ -1,156 +0,0 @@
package oci
import (
"net/http"
"strings"
"testing"
)
const testKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyLnyzmYj0dZuDo2nclIdEyLZrFZLtw5xFldWpCUl5W3SxKDL
iIgwxpSO75Yf++Rzqc5j6S5bpIrdca6AwVXCNmjjxMPO7vLLm4l4IUOZMv5FqKaC
I2plFz9uBkzGscnYnMbsDA430082E07lYpNv1xy8JwpbrIsqIMh4XCKci/Od5sLR
kEicmOpQK42FGRTQjjmQoWtv+9XED+vYTRL0AxQEC/6i/E7yssFXZ+fpHSKWeKTQ
K/1Fc4pZ1zNzJcDXGuweISx/QMLz78TAPH5OBq/EQzHKSpKvfnWFRyBHne8pwuN8
8wzbbD+/7OFjz28jNSERVJvfYe3X1k69IWMNGwIDAQABAoIBAQCZhcdU38AzxSrG
DMfuYymDslsEObiNWQlbig9lWlhCwx26cDVbxrZvm747NvpdgVyJmqbF+UP0dJVs
Voh51qrFTLIwk4bZMXBTFPCBmJ865knG9RuCFOUew8/WF7C82GHJf0eY7OL7xpDY
cbZ2D8gxofOydHSrYoElM88CwSI00xPCbBKEMrBO94oXC8yfp2bmV6bNhVXwFDEM
qda7M6jVAsBrTOzxUF5VdUUU/MLsu2cCk/ap1zer2Bml9Afk1bMeGJ3XDQgol0pS
CLxaGczpSNVMF9+pjA5sFHR5rmcl0b/kC9HsgOJGhLOimtS94O64dSdWifgsjf6+
fhT2SMiRAoGBAOUDwkdzNqQfvS+qrP2ULpB4vt7MZ70rDGmyMDLZ1VWgcm2cDIad
b7MkFG6GCa48mKkFXK9mOPyq8ELoTjZo2p+relEqf49BpaNxr+cp11pX7g0AkzCa
a8LwdOOUW/poqYl2xLuw9Rz6ky6ybzatMvCwpQCwnbAdABIVxz4oQKHpAoGBAOBg
3uYC/ynGdF9gJTfdS5XPYoLcKKRRspBZrvvDHaWyBnanm5KYwDAZPzLQMqcpyPeo
5xgwMmtNlc6lKKyGkhSLNCV+eO3yAx1h/cq7ityvMS7u6o5sq+/bvtEnbUPYbEtk
AhVD7/w5Yyzzi4beiQxDKe0q1mvUAH56aGqJivBjAoGBALmUMTPbDhUzTwg4Y1Rd
ZtpVrj43H31wS+++oEYktTZc/T0LLi9Llr9w5kmlvmR94CtfF/ted6FwF5/wRajb
kQXAXC83pAR/au0mbCeDhWpFRLculxfUmqxuVBozF9G0TGYDY2rA+++OsgQuPebt
tRDL4/nKJQ4Ygf0lvr4EulM5AoGBALoIdyabu3WmfhwJujH8P8wA+ztmUCgVOIio
YwWIe49C8Er2om1ESqxWcmit6CFi6qY0Gw6Z/2OqGxgPJY8NsBZqaBziJF+cdWqq
MWMiZXqdopi4LC9T+KZROn9tQhGrYfaL/5IkFti3t/uwHbH/1f8dvKhQCSGzz4kN
8n7KdTDjAoGAKh8XdIOQlThFK108VT2yp4BGZXEOvWrR19DLbuUzHVrEX+Bd+uFo
Ruk9iKEH7PSnlRnIvWc1y9qN7fUi5OR3LvQNGlXHyUl6CtmH3/b064bBKudC+XTn
VBelcIoGpH7Dn9I6pKUFauJz1TSbQCIjYGNqrjyzLtG+lH/gy5q4xs8=
-----END RSA PRIVATE KEY-----`
type testTarget struct {
CapturedReq *http.Request
Response *http.Response
}
func (target *testTarget) RoundTrip(req *http.Request) (*http.Response, error) {
target.CapturedReq = req
return target.Response, nil
}
func testReq(method string, url string, body string, headers map[string]string) *http.Request {
req, err := http.NewRequest(method, url, strings.NewReader(body))
if err != nil {
panic(err.Error())
}
for k, v := range headers {
req.Header.Add(k, v)
}
return req
}
var KnownGoodCases = []struct {
name string
inputRequest func() *http.Request
expectedHeaders map[string]string
}{
{
name: "Simple GET",
inputRequest: func() *http.Request {
return testReq("GET", "https://example.com/", "", map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
},
expectedHeaders: map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
"content-type": "application/json",
"host": "example.com",
"accept": "application/json",
"Authorization": "Signature headers=\"date host (request-target)\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"UMw/FImQYZ5JBpfYcR9YN72lhupGl5yS+522NS9glLodU9f4oKRqaocpGdSUSRhhSDKxIx01rV547/HemJ6QqEPaJJuDQPXsGthokWMU2DBGyaMAqhLClgCJiRQMwpg4rdL2tETzkM3wy6UN+I52RYoNSdsnat2ZArCkfl8dIl9ydcwD8/+BqB8d2wyaAIS4iagdPKLAC/Mu9OzyUPOXQhYGYsoEdOowOUkHOlob65PFrlHmKJDdjEF3MDcygEpApItf4iUEloP5bjixAbZEVpj3HLQ5uaPx9m+RsLzYMeO0adE0wOv2YNmwZrExGhXh1BpTU33m5amHeUBxSaG+2A==\",version=\"1\"",
},
},
{
name: "Simple PUT request",
inputRequest: func() *http.Request {
return testReq("PUT", "https://example.com/", "Some Content", map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
},
expectedHeaders: map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
"content-type": "application/json",
"content-length": "12",
"x-content-sha256": "lQ8fsURxamLtHxnwTYqd3MNYadJ4ZB/U9yQBKzu/fXA=",
"accept": "application/json",
"Authorization": "Signature headers=\"(request-target) host date content-length content-type x-content-sha256\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"FHyPt4PE2HGH+iftzcViB76pCJ2R9+DdTCo1Ss4mH4KHQJdyQtPsCpe6Dc19zie6cRr6dsenk21yYnncic8OwZhII8DULj2//qLFGmgFi84s7LJqMQ/COiP7O9KtCN+U8MMt4PV7ZDsiGFn3/8EUJ1wxYscxSIB19S1NpuEL062JgGfkqxTkTPd7V3Xh1NlmUUtQrAMR3l56k1iV0zXY9Uw0CjWYjueMP0JUmkO7zycYAVBrx7Q8wkmejlyD7yFrAnObyEsMm9cIL9IcruWFHeCHFxRLslw7AoLxibAm2Dc9EROuvCK2UkUp8AFkE+QyYDMrrSm1NLBMWdnYqdickA==\",version=\"1\"",
},
},
{
name: "Simple POST request",
inputRequest: func() *http.Request {
return testReq("POST", "https://example.com/", "Some Content", map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
},
expectedHeaders: map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
"content-type": "application/json",
"content-length": "12",
"x-content-sha256": "lQ8fsURxamLtHxnwTYqd3MNYadJ4ZB/U9yQBKzu/fXA=",
"accept": "application/json",
"Authorization": "Signature headers=\"(request-target) host date content-length content-type x-content-sha256\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"WzGIoySkjqydwabMTxjVs05UBu0hThAEBzVs7HbYO45o2XpaoqGiNX67mNzs1PeYrGHpJp8+Ysoz66PChWV/1trxuTU92dQ/FgwvcwBRy5dQvdLkjWCZihNunSk4gt9652w6zZg/ybLon0CFbLRnlanDJDX9BgR3ttuTxf30t5qr2A4fnjFF4VjaU/CzE13cNfaWftjSd+xNcla2sbArF3R0+CEEb0xZEPzTyjjjkyvXdaPZwEprVn8IDmdJvLmRP4EniAPxE1EZIhd712M5ondQkR4/WckM44/hlKDeXGFb4y+QnU02i4IWgOWs3dh2tuzS1hp1zfq7qgPbZ4hp0A==\",version=\"1\"",
},
},
{
name: "Simple DELETE",
inputRequest: func() *http.Request {
return testReq("DELETE", "https://example.com/", "Some Content", map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
},
expectedHeaders: map[string]string{
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
"content-type": "application/json",
"accept": "application/json",
"Authorization": "Signature headers=\"date host (request-target)\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"Kj4YSpONZG1cibLbNgxIp4VoS5+80fsB2Fh2Ue28+QyXq4wwrJpMP+8jEupz1yTk1SNPYuxsk7lNOgtI6G1Hq0YJJVum74j46sUwRWe+f08tMJ3c9J+rrzLfpIrakQ8PaudLhHU0eK5kuTZme1dCwRWXvZq3r5IqkGot/OGMabKpBygRv9t0i5ry+bTslSjMqafTWLosY9hgIiGrXD+meB5tpyn+gPVYc//Hc/C7uNNgLJIMk5DKVa4U0YnoY3ojafZTXZQQNGRn2NDMcZUX3f3nJlUIfiZRiOCTkbPwx/fWb4MZtYaEsY5OPficbJRvfOBxSG1wjX+8rgO7ijhMAA==\",version=\"1\"",
},
},
}
func TestKnownGoodRequests(t *testing.T) {
pKey, err := ParsePrivateKey([]byte(testKey), []byte{})
if err != nil {
t.Fatalf("Failed to parse test key: %s", err.Error())
}
config := &Config{
Key: pKey,
User: "testuser",
Tenancy: "tenant",
Fingerprint: "3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55",
}
expectedResponse := &http.Response{}
for _, tt := range KnownGoodCases {
targetBackend := &testTarget{Response: expectedResponse}
target := NewTransport(targetBackend, config)
_, err = target.RoundTrip(tt.inputRequest())
if err != nil {
t.Fatalf("%s: Failed to handle request %s", tt.name, err.Error())
}
sentReq := targetBackend.CapturedReq
for header, val := range tt.expectedHeaders {
if sentReq.Header.Get(header) != val {
t.Fatalf("%s: Header mismatch in response,\n\t expecting \"%s\"\n\t got \"%s\"", tt.name, val, sentReq.Header.Get(header))
}
}
}
}

View File

@ -1,47 +0,0 @@
package oci
import (
"time"
)
// VNICService enables communicating with the OCI compute API's VNICs
// endpoint.
type VNICService struct {
client *baseClient
}
// NewVNICService creates a new VNICService for communicating with the
// OCI compute API's instance related endpoints.
func NewVNICService(s *baseClient) *VNICService {
return &VNICService{client: s.New().Path("vnics/")}
}
// VNIC - a virtual network interface card.
type VNIC struct {
AvailabilityDomain string `json:"availabilityDomain"`
CompartmentID string `json:"compartmentId"`
DisplayName string `json:"displayName,omitempty"`
ID string `json:"id"`
LifecycleState string `json:"lifecycleState"`
PrivateIP string `json:"privateIp"`
PublicIP string `json:"publicIp"`
SubnetID string `json:"subnetId"`
TimeCreated time.Time `json:"timeCreated"`
}
// GetVNICParams are the parameters available when communicating with the
// ListVNICs API endpoint.
type GetVNICParams struct {
ID string `url:"vnicId"`
}
// Get returns an individual VNIC.
func (s *VNICService) Get(params *GetVNICParams) (VNIC, error) {
VNIC := &VNIC{}
e := &APIError{}
_, err := s.client.New().Get(params.ID).Receive(VNIC, e)
err = firstError(err, e)
return *VNIC, err
}

View File

@ -1,52 +0,0 @@
package oci
import (
"time"
)
// VNICAttachmentService enables communicating with the OCI compute API's VNIC
// attachment endpoint.
type VNICAttachmentService struct {
client *baseClient
}
// NewVNICAttachmentService creates a new VNICAttachmentService for communicating with the
// OCI compute API's instance related endpoints.
func NewVNICAttachmentService(s *baseClient) *VNICAttachmentService {
return &VNICAttachmentService{
client: s.New().Path("vnicAttachments/"),
}
}
// VNICAttachment details the attachment of a VNIC to a OCI instance.
type VNICAttachment struct {
AvailabilityDomain string `json:"availabilityDomain"`
CompartmentID string `json:"compartmentId"`
DisplayName string `json:"displayName,omitempty"`
ID string `json:"id"`
InstanceID string `json:"instanceId"`
LifecycleState string `json:"lifecycleState"`
SubnetID string `json:"subnetId"`
TimeCreated time.Time `json:"timeCreated"`
VNICID string `json:"vnicId"`
}
// ListVnicAttachmentsParams are the parameters available when communicating
// with the ListVnicAttachments API endpoint.
type ListVnicAttachmentsParams struct {
AvailabilityDomain string `url:"availabilityDomain,omitempty"`
CompartmentID string `url:"compartmentId"`
InstanceID string `url:"instanceId,omitempty"`
VNICID string `url:"vnicId,omitempty"`
}
// List returns an array of VNICAttachments.
func (s *VNICAttachmentService) List(params *ListVnicAttachmentsParams) ([]VNICAttachment, error) {
vnicAttachments := new([]VNICAttachment)
e := new(APIError)
_, err := s.client.New().Get("").QueryStruct(params).Receive(vnicAttachments, e)
err = firstError(err, e)
return *vnicAttachments, err
}

View File

@ -1,31 +0,0 @@
package oci
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestListVNICAttachments(t *testing.T) {
setup()
defer teardown()
id := "ocid1.image.oc1.phx.a"
mux.HandleFunc("/vnicAttachments/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `[{"id":"%s"}]`, id)
})
params := &ListVnicAttachmentsParams{InstanceID: id}
vnicAttachment, err := client.Compute.VNICAttachments.List(params)
if err != nil {
t.Errorf("Client.Compute.VNICAttachments.List() returned error: %v", err)
}
want := []VNICAttachment{{ID: id}}
if !reflect.DeepEqual(vnicAttachment, want) {
t.Errorf("Client.Compute.VNICAttachments.List() returned %+v, want %+v", vnicAttachment, want)
}
}

View File

@ -1,29 +0,0 @@
package oci
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestGetVNIC(t *testing.T) {
setup()
defer teardown()
id := "ocid1.vnic.oc1.phx.a"
path := fmt.Sprintf("/vnics/%s", id)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"id": "%s"}`, id)
})
vnic, err := client.Compute.VNICs.Get(&GetVNICParams{ID: id})
if err != nil {
t.Errorf("Client.Compute.VNICs.Get() returned error: %v", err)
}
want := &VNIC{ID: id}
if reflect.DeepEqual(vnic, want) {
t.Errorf("Client.Compute.VNICs.Get() returned %+v, want %+v", vnic, want)
}
}

View File

@ -1,59 +0,0 @@
package oci
import (
"fmt"
"time"
)
const (
defaultWaitDurationMS = 5000
defaultMaxRetries = 0
)
type Waiter struct {
WaitDurationMS int
MaxRetries int
}
type WaitableService interface {
GetResourceState(id string) (string, error)
}
func stringSliceContains(slice []string, value string) bool {
for _, elem := range slice {
if elem == value {
return true
}
}
return false
}
// NewWaiter creates a waiter with default wait duration and unlimited retry
// operations.
func NewWaiter() *Waiter {
return &Waiter{WaitDurationMS: defaultWaitDurationMS, MaxRetries: defaultMaxRetries}
}
// WaitForResourceToReachState polls a resource that implements WaitableService
// repeatedly until it reaches a known state or fails if it reaches an
// unexpected state. The duration of the interval and number of polls is
// determined by the Waiter configuration.
func (w *Waiter) WaitForResourceToReachState(svc WaitableService, id string, waitStates []string, terminalState string) error {
for i := 0; w.MaxRetries == 0 || i < w.MaxRetries; i++ {
state, err := svc.GetResourceState(id)
if err != nil {
return err
}
if stringSliceContains(waitStates, state) {
time.Sleep(time.Duration(w.WaitDurationMS) * time.Millisecond)
continue
} else if state == terminalState {
return nil
}
return fmt.Errorf("Unexpected resource state %s, expecting a waiting state %s or terminal state %s ", state, waitStates, terminalState)
}
return fmt.Errorf("Maximum number of retries (%d) exceeded; resource did not reach state %s", w.MaxRetries, terminalState)
}

View File

@ -1,80 +0,0 @@
package oci
import (
"errors"
"fmt"
"testing"
)
const (
ValidID = "ID"
)
type testWaitSvc struct {
states []string
idx int
err error
}
func (tw *testWaitSvc) GetResourceState(id string) (string, error) {
if id != ValidID {
return "", fmt.Errorf("Invalid id %s", id)
}
if tw.err != nil {
return "", tw.err
}
if tw.idx >= len(tw.states) {
panic("Invalid test state")
}
state := tw.states[tw.idx]
tw.idx++
return state, nil
}
func TestReturnsWhenWaitStateIsReachedImmediately(t *testing.T) {
ws := &testWaitSvc{states: []string{"OK"}}
w := NewWaiter()
err := w.WaitForResourceToReachState(ws, ValidID, []string{}, "OK")
if err != nil {
t.Errorf("Failed to reach expected state, got %s", err)
}
}
func TestReturnsWhenResourceWaitsInValidWaitingState(t *testing.T) {
w := &Waiter{WaitDurationMS: 1, MaxRetries: defaultMaxRetries}
ws := &testWaitSvc{states: []string{"WAITING", "OK"}}
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
if err != nil {
t.Errorf("Failed to reach expected state, got %s", err)
}
}
func TestPropagatesErrorFromGetter(t *testing.T) {
w := NewWaiter()
ws := &testWaitSvc{states: []string{}, err: errors.New("ERROR")}
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
if err != ws.err {
t.Errorf("Expected error from getter got %s", err)
}
}
func TestReportsInvalidTransitionStateAsError(t *testing.T) {
w := NewWaiter()
tw := &testWaitSvc{states: []string{"UNKNOWN_STATE"}, err: errors.New("ERROR")}
err := w.WaitForResourceToReachState(tw, ValidID, []string{"WAITING"}, "OK")
if err == nil {
t.Fatal("Expected error from getter")
}
}
func TestErrorsWhenMaxWaitTriesExceeded(t *testing.T) {
w := Waiter{WaitDurationMS: 1, MaxRetries: 1}
ws := &testWaitSvc{states: []string{"WAITING", "OK"}}
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
if err == nil {
t.Fatal("Expecting error but wait terminated")
}
}

View File

@ -9,12 +9,12 @@ import (
"os"
"path/filepath"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
ocicommon "github.com/oracle/oci-go-sdk/common"
"github.com/mitchellh/go-homedir"
)
@ -23,7 +23,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
AccessCfg *client.Config
ConfigProvider ocicommon.ConfigurationProvider
AccessCfgFile string `mapstructure:"access_cfg_file"`
AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"`
@ -67,68 +67,54 @@ func NewConfig(raws ...interface{}) (*Config, error) {
}
// Determine where the SDK config is located
var accessCfgFile string
if c.AccessCfgFile != "" {
accessCfgFile = c.AccessCfgFile
} else {
accessCfgFile, err = getDefaultOCISettingsPath()
if c.AccessCfgFile == "" {
c.AccessCfgFile, err = getDefaultOCISettingsPath()
if err != nil {
accessCfgFile = "" // Access cfg might be in template
log.Println("Default OCI settings file not found")
}
}
accessCfg := &client.Config{}
if accessCfgFile != "" {
loadedAccessCfgs, err := client.LoadConfigsFromFile(accessCfgFile)
if err != nil {
return nil, fmt.Errorf("Invalid config file %s: %s", accessCfgFile, err)
}
cfgAccount := "DEFAULT"
if c.AccessCfgFileAccount != "" {
cfgAccount = c.AccessCfgFileAccount
}
var ok bool
accessCfg, ok = loadedAccessCfgs[cfgAccount]
if !ok {
return nil, fmt.Errorf("No account section '%s' found in config file %s", cfgAccount, accessCfgFile)
}
}
// Override SDK client config with any non-empty template properties
if c.UserID != "" {
accessCfg.User = c.UserID
}
if c.TenancyID != "" {
accessCfg.Tenancy = c.TenancyID
}
if c.Region != "" {
accessCfg.Region = c.Region
}
// Default if the template nor the API config contains a region.
if accessCfg.Region == "" {
accessCfg.Region = "us-phoenix-1"
}
if c.Fingerprint != "" {
accessCfg.Fingerprint = c.Fingerprint
}
if c.PassPhrase != "" {
accessCfg.PassPhrase = c.PassPhrase
if c.AccessCfgFileAccount == "" {
c.AccessCfgFileAccount = "DEFAULT"
}
var keyContent []byte
if c.KeyFile != "" {
accessCfg.KeyFile = c.KeyFile
accessCfg.Key, err = client.LoadPrivateKey(accessCfg)
path, err := homedir.Expand(c.KeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to load private key %s : %s", accessCfg.KeyFile, err)
return nil, err
}
// Read API signing key
keyContent, err = ioutil.ReadFile(path)
if err != nil {
return nil, err
}
}
fileProvider, _ := ocicommon.ConfigurationProviderFromFileWithProfile(c.AccessCfgFile, c.AccessCfgFileAccount, c.PassPhrase)
if c.Region == "" {
var region string
if fileProvider != nil {
region, _ = fileProvider.Region()
}
if region == "" {
c.Region = "us-phoenix-1"
}
}
providers := []ocicommon.ConfigurationProvider{
NewRawConfigurationProvider(c.TenancyID, c.UserID, c.Region, c.Fingerprint, string(keyContent), &c.PassPhrase),
}
if fileProvider != nil {
providers = append(providers, fileProvider)
}
// Load API access configuration from SDK
configProvider, err := ocicommon.ComposingConfigurationProvider(providers)
if err != nil {
return nil, err
}
var errs *packer.MultiError
@ -136,44 +122,36 @@ func NewConfig(raws ...interface{}) (*Config, error) {
errs = packer.MultiErrorAppend(errs, es...)
}
// Required AccessCfg configuration options
if accessCfg.User == "" {
if userOCID, _ := configProvider.UserOCID(); userOCID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("'user_ocid' must be specified"))
}
if accessCfg.Tenancy == "" {
tenancyOCID, _ := configProvider.TenancyOCID()
if tenancyOCID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("'tenancy_ocid' must be specified"))
}
if accessCfg.Region == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("'region' must be specified"))
}
if accessCfg.Fingerprint == "" {
if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("'fingerprint' must be specified"))
}
if accessCfg.Key == nil {
if _, err := configProvider.PrivateRSAKey(); err != nil {
errs = packer.MultiErrorAppend(
errs, errors.New("'key_file' must be specified"))
}
c.AccessCfg = accessCfg
// Required non AccessCfg configuration options
c.ConfigProvider = configProvider
if c.AvailabilityDomain == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("'availability_domain' must be specified"))
}
if c.CompartmentID == "" {
c.CompartmentID = accessCfg.Tenancy
if c.CompartmentID == "" && tenancyOCID != "" {
c.CompartmentID = tenancyOCID
}
if c.Shape == "" {

View File

@ -0,0 +1,76 @@
package oci
import (
"crypto/rsa"
"errors"
"fmt"
"github.com/oracle/oci-go-sdk/common"
)
// rawConfigurationProvider allows a user to simply construct a configuration
// provider from raw values. It errors on access when those values are empty.
type rawConfigurationProvider struct {
tenancy string
user string
region string
fingerprint string
privateKey string
privateKeyPassphrase *string
}
// NewRawConfigurationProvider will create a rawConfigurationProvider.
func NewRawConfigurationProvider(tenancy, user, region, fingerprint, privateKey string, privateKeyPassphrase *string) common.ConfigurationProvider {
return rawConfigurationProvider{tenancy, user, region, fingerprint, privateKey, privateKeyPassphrase}
}
func (p rawConfigurationProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
return common.PrivateKeyFromBytes([]byte(p.privateKey), p.privateKeyPassphrase)
}
func (p rawConfigurationProvider) KeyID() (keyID string, err error) {
tenancy, err := p.TenancyOCID()
if err != nil {
return
}
user, err := p.UserOCID()
if err != nil {
return
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return
}
return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
}
func (p rawConfigurationProvider) TenancyOCID() (string, error) {
if p.tenancy == "" {
return "", errors.New("no tenancy provided")
}
return p.tenancy, nil
}
func (p rawConfigurationProvider) UserOCID() (string, error) {
if p.user == "" {
return "", errors.New("no user provided")
}
return p.user, nil
}
func (p rawConfigurationProvider) KeyFingerprint() (string, error) {
if p.fingerprint == "" {
return "", errors.New("no fingerprint provided")
}
return p.fingerprint, nil
}
func (p rawConfigurationProvider) Region() (string, error) {
if p.region == "" {
return "", errors.New("no region provided")
}
return p.region, nil
}

View File

@ -1,13 +1,16 @@
package oci
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
"github.com/go-ini/ini"
)
func testConfig(accessConfFile *os.File) map[string]interface{} {
@ -29,22 +32,16 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
}
}
func getField(c *client.Config, field string) string {
r := reflect.ValueOf(c)
f := reflect.Indirect(r).FieldByName(field)
return string(f.String())
}
func TestConfig(t *testing.T) {
// Shared set-up and defered deletion
cfg, keyFile, err := client.BaseTestConfig()
cfg, keyFile, err := baseTestConfigWithTmpKeyFile()
if err != nil {
t.Fatal(err)
}
defer os.Remove(keyFile.Name())
cfgFile, err := client.WriteTestConfig(cfg)
cfgFile, err := writeTestConfig(cfg)
if err != nil {
t.Fatal(err)
}
@ -55,7 +52,7 @@ func TestConfig(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "packer_config_test")
if err != nil {
t.Fatalf("err: %+v", err)
t.Fatalf("Unexpected error when creating temporary directory: %+v", err)
}
defer os.Remove(tmpHome)
@ -64,15 +61,13 @@ func TestConfig(t *testing.T) {
defer os.Setenv("HOME", home)
// Config tests
t.Run("BaseConfig", func(t *testing.T) {
raw := testConfig(cfgFile)
_, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("err: %+v", errs)
t.Fatalf("Unexpected error in configuration %+v", errs)
}
})
t.Run("NoAccessConfig", func(t *testing.T) {
@ -81,14 +76,14 @@ func TestConfig(t *testing.T) {
_, errs := NewConfig(raw)
s := errs.Error()
expectedErrors := []string{
"'user_ocid'", "'tenancy_ocid'", "'fingerprint'",
"'key_file'",
"'user_ocid'", "'tenancy_ocid'", "'fingerprint'", "'key_file'",
}
s := errs.Error()
for _, expected := range expectedErrors {
if !strings.Contains(s, expected) {
t.Errorf("Expected %s to contain '%s'", s, expected)
t.Errorf("Expected %q to contain '%s'", s, expected)
}
}
})
@ -113,12 +108,17 @@ func TestConfig(t *testing.T) {
raw := testConfig(cfgFile)
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("err: %+v", errs)
t.Fatalf("Unexpected error in configuration %+v", errs)
}
expected := "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if c.AccessCfg.Tenancy != expected {
t.Errorf("Expected tenancy: %s, got %s.", expected, c.AccessCfg.Tenancy)
tenancy, err := c.ConfigProvider.TenancyOCID()
if err != nil {
t.Fatalf("Unexpected error getting tenancy ocid: %v", err)
}
expected := "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if tenancy != expected {
t.Errorf("Expected tenancy: %s, got %s.", expected, tenancy)
}
})
@ -127,12 +127,17 @@ func TestConfig(t *testing.T) {
raw := testConfig(cfgFile)
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("err: %+v", errs)
t.Fatalf("Unexpected error in configuration %+v", errs)
}
region, err := c.ConfigProvider.Region()
if err != nil {
t.Fatalf("Unexpected error getting region: %v", err)
}
expected := "us-ashburn-1"
if c.AccessCfg.Region != expected {
t.Errorf("Expected region: %s, got %s.", expected, c.AccessCfg.Region)
if region != expected {
t.Errorf("Expected region: %s, got %s.", expected, region)
}
})
@ -159,7 +164,7 @@ func TestConfig(t *testing.T) {
c, errs := NewConfig(raw)
if errs != nil {
t.Errorf("Unexpected error(s): %s", errs)
t.Fatalf("Unexpected error in configuration %+v", errs)
}
if !strings.Contains(c.ImageName, "packer-") {
@ -167,30 +172,138 @@ func TestConfig(t *testing.T) {
}
})
// Test that AccessCfgFile properties are overridden by their
// corresponding template keys.
accessOverrides := map[string]string{
"user_ocid": "User",
"tenancy_ocid": "Tenancy",
"region": "Region",
"fingerprint": "Fingerprint",
}
for k, v := range accessOverrides {
t.Run("AccessCfg."+v+"Overridden", func(t *testing.T) {
expected := "override"
t.Run("user_ocid_overridden", func(t *testing.T) {
expected := "override"
raw := testConfig(cfgFile)
raw["user_ocid"] = expected
raw := testConfig(cfgFile)
raw[k] = expected
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration %+v", errs)
}
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("err: %+v", errs)
}
user, _ := c.ConfigProvider.UserOCID()
if user != expected {
t.Errorf("Expected ConfigProvider.UserOCID: %s, got %s", expected, user)
}
})
accessVal := getField(c.AccessCfg, v)
if accessVal != expected {
t.Errorf("Expected AccessCfg.%s: %s, got %s", v, expected, accessVal)
}
})
}
t.Run("tenancy_ocid_overidden", func(t *testing.T) {
expected := "override"
raw := testConfig(cfgFile)
raw["tenancy_ocid"] = expected
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration %+v", errs)
}
tenancy, _ := c.ConfigProvider.TenancyOCID()
if tenancy != expected {
t.Errorf("Expected ConfigProvider.TenancyOCID: %s, got %s", expected, tenancy)
}
})
t.Run("region_overidden", func(t *testing.T) {
expected := "override"
raw := testConfig(cfgFile)
raw["region"] = expected
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration %+v", errs)
}
region, _ := c.ConfigProvider.Region()
if region != expected {
t.Errorf("Expected ConfigProvider.Region: %s, got %s", expected, region)
}
})
t.Run("fingerprint_overidden", func(t *testing.T) {
expected := "override"
raw := testConfig(cfgFile)
raw["fingerprint"] = expected
c, errs := NewConfig(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration: %+v", errs)
}
fingerprint, _ := c.ConfigProvider.KeyFingerprint()
if fingerprint != expected {
t.Errorf("Expected ConfigProvider.KeyFingerprint: %s, got %s", expected, fingerprint)
}
})
}
// BaseTestConfig creates the base (DEFAULT) config including a temporary key
// file.
// NOTE: Caller is responsible for removing temporary key file.
func baseTestConfigWithTmpKeyFile() (*ini.File, *os.File, error) {
keyFile, err := generateRSAKeyFile()
if err != nil {
return nil, keyFile, err
}
// Build ini
cfg := ini.Empty()
section, _ := cfg.NewSection("DEFAULT")
section.NewKey("region", "us-ashburn-1")
section.NewKey("tenancy", "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
section.NewKey("user", "ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
section.NewKey("fingerprint", "70:04:5z:b3:19:ab:90:75:a4:1f:50:d4:c7:c3:33:20")
section.NewKey("key_file", keyFile.Name())
return cfg, keyFile, nil
}
// WriteTestConfig writes a ini.File to a temporary file for use in unit tests.
// NOTE: Caller is responsible for removing temporary file.
func writeTestConfig(cfg *ini.File) (*os.File, error) {
confFile, err := ioutil.TempFile("", "config_file")
if err != nil {
return nil, err
}
if _, err := confFile.Write([]byte("[DEFAULT]\n")); err != nil {
os.Remove(confFile.Name())
return nil, err
}
if _, err := cfg.WriteTo(confFile); err != nil {
os.Remove(confFile.Name())
return nil, err
}
return confFile, nil
}
// generateRSAKeyFile generates an RSA key file for use in unit tests.
// NOTE: The caller is responsible for deleting the temporary file.
func generateRSAKeyFile() (*os.File, error) {
// Create temporary file for the key
f, err := ioutil.TempFile("", "key")
if err != nil {
return nil, err
}
// Generate key
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
return nil, err
}
// ASN.1 DER encoded form
privDer := x509.MarshalPKCS1PrivateKey(priv)
privBlk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privDer,
}
// Write the key out
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
return nil, err
}
return f, nil
}

View File

@ -1,13 +1,11 @@
package oci
import (
client "github.com/hashicorp/packer/builder/oracle/oci/client"
)
import "github.com/oracle/oci-go-sdk/core"
// Driver interfaces between the builder steps and the OCI SDK.
type Driver interface {
CreateInstance(publicKey string) (string, error)
CreateImage(id string) (client.Image, error)
CreateImage(id string) (core.Image, error)
DeleteImage(id string) error
GetInstanceIP(id string) (string, error)
TerminateInstance(id string) error

View File

@ -1,8 +1,6 @@
package oci
import (
client "github.com/hashicorp/packer/builder/oracle/oci/client"
)
import "github.com/oracle/oci-go-sdk/core"
// driverMock implements the Driver interface and communicates with Oracle
// OCI.
@ -40,12 +38,12 @@ func (d *driverMock) CreateInstance(publicKey string) (string, error) {
}
// CreateImage creates a new custom image.
func (d *driverMock) CreateImage(id string) (client.Image, error) {
func (d *driverMock) CreateImage(id string) (core.Image, error) {
if d.CreateImageErr != nil {
return client.Image{}, d.CreateImageErr
return core.Image{}, d.CreateImageErr
}
d.CreateImageID = id
return client.Image{ID: id}, nil
return core.Image{Id: &id}, nil
}
// DeleteImage mocks deleting a custom image.

View File

@ -1,123 +1,191 @@
package oci
import (
"context"
"errors"
"fmt"
"time"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
core "github.com/oracle/oci-go-sdk/core"
)
// driverOCI implements the Driver interface and communicates with Oracle
// OCI.
type driverOCI struct {
client *client.Client
cfg *Config
computeClient core.ComputeClient
vcnClient core.VirtualNetworkClient
cfg *Config
}
// NewDriverOCI Creates a new driverOCI with a connected client.
// NewDriverOCI Creates a new driverOCI with a connected compute client and a connected vcn client.
func NewDriverOCI(cfg *Config) (Driver, error) {
client, err := client.NewClient(cfg.AccessCfg)
coreClient, err := core.NewComputeClientWithConfigurationProvider(cfg.ConfigProvider)
if err != nil {
return nil, err
}
return &driverOCI{client: client, cfg: cfg}, nil
vcnClient, err := core.NewVirtualNetworkClientWithConfigurationProvider(cfg.ConfigProvider)
if err != nil {
return nil, err
}
return &driverOCI{
computeClient: coreClient,
vcnClient: vcnClient,
cfg: cfg,
}, nil
}
// CreateInstance creates a new compute instance.
func (d *driverOCI) CreateInstance(publicKey string) (string, error) {
params := &client.LaunchInstanceParams{
AvailabilityDomain: d.cfg.AvailabilityDomain,
CompartmentID: d.cfg.CompartmentID,
ImageID: d.cfg.BaseImageID,
Shape: d.cfg.Shape,
SubnetID: d.cfg.SubnetID,
Metadata: map[string]string{
"ssh_authorized_keys": publicKey,
},
metadata := map[string]string{
"ssh_authorized_keys": publicKey,
}
if d.cfg.UserData != "" {
params.Metadata["user_data"] = d.cfg.UserData
metadata["user_data"] = d.cfg.UserData
}
instance, err := d.client.Compute.Instances.Launch(params)
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: core.LaunchInstanceDetails{
AvailabilityDomain: &d.cfg.AvailabilityDomain,
CompartmentId: &d.cfg.CompartmentID,
ImageId: &d.cfg.BaseImageID,
Shape: &d.cfg.Shape,
SubnetId: &d.cfg.SubnetID,
Metadata: metadata,
}})
if err != nil {
return "", err
}
return instance.ID, nil
return *instance.Id, nil
}
// CreateImage creates a new custom image.
func (d *driverOCI) CreateImage(id string) (client.Image, error) {
params := &client.CreateImageParams{
CompartmentID: d.cfg.CompartmentID,
InstanceID: id,
DisplayName: d.cfg.ImageName,
}
image, err := d.client.Compute.Images.Create(params)
func (d *driverOCI) CreateImage(id string) (core.Image, error) {
res, err := d.computeClient.CreateImage(context.TODO(), core.CreateImageRequest{CreateImageDetails: core.CreateImageDetails{
CompartmentId: &d.cfg.CompartmentID,
InstanceId: &id,
DisplayName: &d.cfg.ImageName,
}})
if err != nil {
return client.Image{}, err
return core.Image{}, err
}
return image, nil
return res.Image, nil
}
// DeleteImage deletes a custom image.
func (d *driverOCI) DeleteImage(id string) error {
return d.client.Compute.Images.Delete(&client.DeleteImageParams{ID: id})
_, err := d.computeClient.DeleteImage(context.TODO(), core.DeleteImageRequest{ImageId: &id})
return err
}
// GetInstanceIP returns the public or private IP corresponding to the given instance id.
func (d *driverOCI) GetInstanceIP(id string) (string, error) {
// get nvic and cross ref to find pub ip address
vnics, err := d.client.Compute.VNICAttachments.List(
&client.ListVnicAttachmentsParams{
InstanceID: id,
CompartmentID: d.cfg.CompartmentID,
},
)
vnics, err := d.computeClient.ListVnicAttachments(context.TODO(), core.ListVnicAttachmentsRequest{
InstanceId: &id,
CompartmentId: &d.cfg.CompartmentID,
})
if err != nil {
return "", err
}
if len(vnics) < 1 {
if len(vnics.Items) == 0 {
return "", errors.New("instance has zero VNICs")
}
vnic, err := d.client.Compute.VNICs.Get(&client.GetVNICParams{ID: vnics[0].VNICID})
vnic, err := d.vcnClient.GetVnic(context.TODO(), core.GetVnicRequest{VnicId: vnics.Items[0].VnicId})
if err != nil {
return "", fmt.Errorf("Error getting VNIC details: %s", err)
}
if d.cfg.UsePrivateIP {
return vnic.PrivateIP, nil
return *vnic.PrivateIp, nil
}
return vnic.PublicIP, nil
if vnic.PublicIp == nil {
return "", fmt.Errorf("Error getting VNIC Public Ip for: %s", id)
}
return *vnic.PublicIp, nil
}
// TerminateInstance terminates a compute instance.
func (d *driverOCI) TerminateInstance(id string) error {
params := &client.TerminateInstanceParams{ID: id}
return d.client.Compute.Instances.Terminate(params)
_, err := d.computeClient.TerminateInstance(context.TODO(), core.TerminateInstanceRequest{
InstanceId: &id,
})
return err
}
// WaitForImageCreation waits for a provisioning custom image to reach the
// "AVAILABLE" state.
func (d *driverOCI) WaitForImageCreation(id string) error {
return client.NewWaiter().WaitForResourceToReachState(
d.client.Compute.Images,
return waitForResourceToReachState(
func(string) (string, error) {
image, err := d.computeClient.GetImage(context.TODO(), core.GetImageRequest{ImageId: &id})
if err != nil {
return "", err
}
return string(image.LifecycleState), nil
},
id,
[]string{"PROVISIONING"},
"AVAILABLE",
0, //Unlimited Retries
5*time.Second, //5 second wait between retries
)
}
// WaitForInstanceState waits for an instance to reach the a given terminal
// state.
func (d *driverOCI) WaitForInstanceState(id string, waitStates []string, terminalState string) error {
return client.NewWaiter().WaitForResourceToReachState(
d.client.Compute.Instances,
return waitForResourceToReachState(
func(string) (string, error) {
instance, err := d.computeClient.GetInstance(context.TODO(), core.GetInstanceRequest{InstanceId: &id})
if err != nil {
return "", err
}
return string(instance.LifecycleState), nil
},
id,
waitStates,
terminalState,
0, //Unlimited Retries
5*time.Second, //5 second wait between retries
)
}
// WaitForResourceToReachState checks the response of a request through a
// polled get and waits until the desired state or until the max retried has
// been reached.
func waitForResourceToReachState(getResourceState func(string) (string, error), id string, waitStates []string, terminalState string, maxRetries int, waitDuration time.Duration) error {
for i := 0; maxRetries == 0 || i < maxRetries; i++ {
state, err := getResourceState(id)
if err != nil {
return err
}
if stringSliceContains(waitStates, state) {
time.Sleep(waitDuration)
continue
} else if state == terminalState {
return nil
}
return fmt.Errorf("Unexpected resource state %q, expecting a waiting state %s or terminal state %q ", state, waitStates, terminalState)
}
return fmt.Errorf("Maximum number of retries (%d) exceeded; resource did not reach state %q", maxRetries, terminalState)
}
// stringSliceContains loops through a slice of strings returning a boolean
// based on whether a given value is contained in the slice.
func stringSliceContains(slice []string, value string) bool {
for _, elem := range slice {
if elem == value {
return true
}
}
return false
}

View File

@ -27,7 +27,7 @@ func (s *stepImage) Run(_ context.Context, state multistep.StateBag) multistep.S
return multistep.ActionHalt
}
err = driver.WaitForImageCreation(image.ID)
err = driver.WaitForImageCreation(*image.Id)
if err != nil {
err = fmt.Errorf("Error waiting for image creation to finish: %s", err)
ui.Error(err.Error())

View File

@ -6,33 +6,32 @@ import (
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
)
// TODO(apryde): It would be good not to have to write a key file to disk to
// load the config.
func baseTestConfig() *Config {
_, keyFile, err := client.BaseTestConfig()
_, keyFile, err := baseTestConfigWithTmpKeyFile()
if err != nil {
panic(err)
}
cfg, err := NewConfig(map[string]interface{}{
"availability_domain": "aaaa:PHX-AD-3",
"availability_domain": "aaaa:US-ASHBURN-AD-1",
// Image
"base_image_ocid": "ocd1...",
"base_image_ocid": "ocid1.image.oc1.iad.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"shape": "VM.Standard1.1",
"image_name": "HelloWorld",
"region": "us-ashburn-1",
// Networking
"subnet_ocid": "ocd1...",
"subnet_ocid": "ocid1.subnet.oc1.iad.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
// AccessConfig
"user_ocid": "ocid1...",
"tenancy_ocid": "ocid1...",
"fingerprint": "00:00...",
"user_ocid": "ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"tenancy_ocid": "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"fingerprint": "70:04:5z:b3:19:ab:90:75:a4:1f:50:d4:c7:c3:33:20",
"key_file": keyFile.Name(),
// Comm