Migrate to official OCI Go SDK
This commit is contained in:
parent
31973d9f8b
commit
c442ba165e
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue