added instance principals support for oci builder
This commit is contained in:
parent
1c27d9d04b
commit
0f2cb45fc6
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
ocicommon "github.com/oracle/oci-go-sdk/common"
|
||||
ociauth "github.com/oracle/oci-go-sdk/common/auth"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
@ -27,6 +28,18 @@ type Config struct {
|
|||
|
||||
configProvider ocicommon.ConfigurationProvider
|
||||
|
||||
// Instance Principals (OPTIONAL)
|
||||
// If set to true the following can't have non empty values
|
||||
// - AccessCfgFile
|
||||
// - AccessCfgFileAccount
|
||||
// - UserID
|
||||
// - TenancyID
|
||||
// - Region
|
||||
// - Fingerprint
|
||||
// - KeyFile
|
||||
// - PassPhrase
|
||||
InstancePrincipals bool `mapstructure:"use_instance_principals"`
|
||||
|
||||
AccessCfgFile string `mapstructure:"access_cfg_file"`
|
||||
AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"`
|
||||
|
||||
|
@ -86,84 +99,134 @@ func (c *Config) Prepare(raws ...interface{}) error {
|
|||
return fmt.Errorf("Failed to mapstructure Config: %+v", err)
|
||||
}
|
||||
|
||||
// Determine where the SDK config is located
|
||||
if c.AccessCfgFile == "" {
|
||||
c.AccessCfgFile, err = getDefaultOCISettingsPath()
|
||||
if err != nil {
|
||||
log.Println("Default OCI settings file not found")
|
||||
}
|
||||
}
|
||||
|
||||
if c.AccessCfgFileAccount == "" {
|
||||
c.AccessCfgFileAccount = "DEFAULT"
|
||||
}
|
||||
|
||||
var keyContent []byte
|
||||
if c.KeyFile != "" {
|
||||
path, err := packer.ExpandUser(c.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read API signing key
|
||||
keyContent, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return 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 err
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
|
||||
if userOCID, _ := configProvider.UserOCID(); userOCID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'user_ocid' must be specified"))
|
||||
}
|
||||
var tenancyOCID string
|
||||
|
||||
tenancyOCID, _ := configProvider.TenancyOCID()
|
||||
if tenancyOCID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'tenancy_ocid' must be specified"))
|
||||
}
|
||||
if c.InstancePrincipals {
|
||||
// We could go through all keys in one go and report that the below set
|
||||
// of keys cannot coexist with use_instance_principals but decided to
|
||||
// split them and report them seperately so that the user sees the specific
|
||||
// key involved.
|
||||
var message string = " cannot be present when use_instance_principals is set to true."
|
||||
if c.AccessCfgFile != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file"+message))
|
||||
}
|
||||
if c.AccessCfgFileAccount != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file_account"+message))
|
||||
}
|
||||
if c.UserID != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("user_ocid"+message))
|
||||
}
|
||||
if c.TenancyID != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("tenancy_ocid"+message))
|
||||
}
|
||||
if c.Region != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("region"+message))
|
||||
}
|
||||
if c.Fingerprint != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("fingerprint"+message))
|
||||
}
|
||||
if c.KeyFile != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("key_file"+message))
|
||||
}
|
||||
if c.PassPhrase != "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("pass_phrase"+message))
|
||||
}
|
||||
// This check is used to facilitate testing. During testing a Mock struct
|
||||
// is assigned to c.configProvider otherwise testing fails because Instance
|
||||
// Principals cannot be obtained.
|
||||
if c.configProvider == nil {
|
||||
// Even though the previous configuraion checks might fail we don't want
|
||||
// to skip this step. It seems that the logic behind the checks in this
|
||||
// file is to check everything even getting the configProvider.
|
||||
c.configProvider, err = ociauth.InstancePrincipalConfigurationProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tenancyOCID, err = c.configProvider.TenancyOCID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Determine where the SDK config is located
|
||||
if c.AccessCfgFile == "" {
|
||||
c.AccessCfgFile, err = getDefaultOCISettingsPath()
|
||||
if err != nil {
|
||||
log.Println("Default OCI settings file not found")
|
||||
}
|
||||
}
|
||||
|
||||
if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'fingerprint' must be specified"))
|
||||
}
|
||||
if c.AccessCfgFileAccount == "" {
|
||||
c.AccessCfgFileAccount = "DEFAULT"
|
||||
}
|
||||
|
||||
if _, err := configProvider.PrivateRSAKey(); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'key_file' must be specified"))
|
||||
}
|
||||
var keyContent []byte
|
||||
if c.KeyFile != "" {
|
||||
path, err := packer.ExpandUser(c.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.configProvider = configProvider
|
||||
// Read API signing key
|
||||
keyContent, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return 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 err
|
||||
}
|
||||
|
||||
if userOCID, _ := configProvider.UserOCID(); userOCID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'user_ocid' must be specified"))
|
||||
}
|
||||
|
||||
tenancyOCID, _ = configProvider.TenancyOCID()
|
||||
if tenancyOCID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'tenancy_ocid' must be specified"))
|
||||
}
|
||||
|
||||
if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'fingerprint' must be specified"))
|
||||
}
|
||||
|
||||
if _, err := configProvider.PrivateRSAKey(); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'key_file' must be specified"))
|
||||
}
|
||||
|
||||
c.configProvider = configProvider
|
||||
}
|
||||
|
||||
if c.AvailabilityDomain == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
|
|
|
@ -56,6 +56,7 @@ type FlatConfig struct {
|
|||
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl"`
|
||||
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure"`
|
||||
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm"`
|
||||
InstancePrincipals *bool `mapstructure:"use_instance_principals" cty:"use_instance_principals"`
|
||||
AccessCfgFile *string `mapstructure:"access_cfg_file" cty:"access_cfg_file"`
|
||||
AccessCfgFileAccount *string `mapstructure:"access_cfg_file_account" cty:"access_cfg_file_account"`
|
||||
UserID *string `mapstructure:"user_ocid" cty:"user_ocid"`
|
||||
|
@ -138,6 +139,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
|
||||
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
|
||||
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
|
||||
"use_instance_principals": &hcldec.AttrSpec{Name: "use_instance_principals", Type: cty.Bool, Required: false},
|
||||
"access_cfg_file": &hcldec.AttrSpec{Name: "access_cfg_file", Type: cty.String, Required: false},
|
||||
"access_cfg_file_account": &hcldec.AttrSpec{Name: "access_cfg_file_account", Type: cty.String, Required: false},
|
||||
"user_ocid": &hcldec.AttrSpec{Name: "user_ocid", Type: cty.String, Required: false},
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
func testConfig(accessConfFile *os.File) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
|
||||
"availability_domain": "aaaa:PHX-AD-3",
|
||||
"access_cfg_file": accessConfFile.Name(),
|
||||
|
||||
|
@ -252,6 +253,36 @@ func TestConfig(t *testing.T) {
|
|||
t.Errorf("Expected ConfigProvider.KeyFingerprint: %s, got %s", expected, fingerprint)
|
||||
}
|
||||
})
|
||||
|
||||
// Test the correct errors are produced when certain template keys
|
||||
// are present alongside use_instance_principals key.
|
||||
invalidKeys := []string{
|
||||
"access_cfg_file",
|
||||
"access_cfg_file_account",
|
||||
"user_ocid",
|
||||
"tenancy_ocid",
|
||||
"region",
|
||||
"fingerprint",
|
||||
"key_file",
|
||||
"pass_phrase",
|
||||
}
|
||||
for _, k := range invalidKeys {
|
||||
t.Run(k+"_mixed_with_use_instance_principals", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
raw["use_instance_principals"] = "true"
|
||||
raw[k] = "some_random_value"
|
||||
|
||||
var c Config
|
||||
|
||||
c.configProvider = instancePrincipalConfigurationProviderMock{}
|
||||
|
||||
errs := c.Prepare(raw)
|
||||
|
||||
if !strings.Contains(errs.Error(), k) {
|
||||
t.Errorf("Expected '%s' to contain '%s'", errs.Error(), k)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BaseTestConfig creates the base (DEFAULT) config including a temporary key
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package oci
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
)
|
||||
|
||||
// Mock struct to be used during testing to obtain Instance Principals.
|
||||
type instancePrincipalConfigurationProviderMock struct {
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) PrivateRSAKey() (*rsa.PrivateKey, error) {
|
||||
return rsa.GenerateKey(rand.Reader, 1024)
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) KeyID() (string, error) {
|
||||
return "some_random_key_id", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) TenancyOCID() (string, error) {
|
||||
return "some_random_tenancy", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) UserOCID() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) KeyFingerprint() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProviderMock) Region() (string, error) {
|
||||
return "some_random_region", nil
|
||||
}
|
158
vendor/github.com/oracle/oci-go-sdk/common/auth/certificate_retriever.go
generated
vendored
Normal file
158
vendor/github.com/oracle/oci-go-sdk/common/auth/certificate_retriever.go
generated
vendored
Normal file
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// x509CertificateRetriever provides an X509 certificate with the RSA private key
|
||||
type x509CertificateRetriever interface {
|
||||
Refresh() error
|
||||
CertificatePemRaw() []byte
|
||||
Certificate() *x509.Certificate
|
||||
PrivateKeyPemRaw() []byte
|
||||
PrivateKey() *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// urlBasedX509CertificateRetriever retrieves PEM-encoded X509 certificates from the given URLs.
|
||||
type urlBasedX509CertificateRetriever struct {
|
||||
certURL string
|
||||
privateKeyURL string
|
||||
passphrase string
|
||||
certificatePemRaw []byte
|
||||
certificate *x509.Certificate
|
||||
privateKeyPemRaw []byte
|
||||
privateKey *rsa.PrivateKey
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func newURLBasedX509CertificateRetriever(certURL, privateKeyURL, passphrase string) x509CertificateRetriever {
|
||||
return &urlBasedX509CertificateRetriever{
|
||||
certURL: certURL,
|
||||
privateKeyURL: privateKeyURL,
|
||||
passphrase: passphrase,
|
||||
mux: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh() is failure atomic, i.e., CertificatePemRaw(), Certificate(), PrivateKeyPemRaw(), and PrivateKey() would
|
||||
// return their previous values if Refresh() fails.
|
||||
func (r *urlBasedX509CertificateRetriever) Refresh() error {
|
||||
common.Debugln("Refreshing certificate")
|
||||
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
var certificatePemRaw []byte
|
||||
var certificate *x509.Certificate
|
||||
if certificatePemRaw, certificate, err = r.renewCertificate(r.certURL); err != nil {
|
||||
return fmt.Errorf("failed to renew certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
var privateKeyPemRaw []byte
|
||||
var privateKey *rsa.PrivateKey
|
||||
if r.privateKeyURL != "" {
|
||||
if privateKeyPemRaw, privateKey, err = r.renewPrivateKey(r.privateKeyURL, r.passphrase); err != nil {
|
||||
return fmt.Errorf("failed to renew private key: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
r.certificatePemRaw = certificatePemRaw
|
||||
r.certificate = certificate
|
||||
r.privateKeyPemRaw = privateKeyPemRaw
|
||||
r.privateKey = privateKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) renewCertificate(url string) (certificatePemRaw []byte, certificate *x509.Certificate, err error) {
|
||||
var body bytes.Buffer
|
||||
if body, err = httpGet(url); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get certificate from %s: %s", url, err.Error())
|
||||
}
|
||||
|
||||
certificatePemRaw = body.Bytes()
|
||||
var block *pem.Block
|
||||
block, _ = pem.Decode(certificatePemRaw)
|
||||
if block == nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse the new certificate, not valid pem data")
|
||||
}
|
||||
|
||||
if certificate, err = x509.ParseCertificate(block.Bytes); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse the new certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
return certificatePemRaw, certificate, nil
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) renewPrivateKey(url, passphrase string) (privateKeyPemRaw []byte, privateKey *rsa.PrivateKey, err error) {
|
||||
var body bytes.Buffer
|
||||
if body, err = httpGet(url); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get private key from %s: %s", url, err.Error())
|
||||
}
|
||||
|
||||
privateKeyPemRaw = body.Bytes()
|
||||
if privateKey, err = common.PrivateKeyFromBytes(privateKeyPemRaw, &passphrase); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse the new private key: %s", err.Error())
|
||||
}
|
||||
|
||||
return privateKeyPemRaw, privateKey, nil
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) CertificatePemRaw() []byte {
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
|
||||
if r.certificatePemRaw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := make([]byte, len(r.certificatePemRaw))
|
||||
copy(c, r.certificatePemRaw)
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) Certificate() *x509.Certificate {
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
|
||||
if r.certificate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := *r.certificate
|
||||
return &c
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) PrivateKeyPemRaw() []byte {
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
|
||||
if r.privateKeyPemRaw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := make([]byte, len(r.privateKeyPemRaw))
|
||||
copy(c, r.privateKeyPemRaw)
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *urlBasedX509CertificateRetriever) PrivateKey() *rsa.PrivateKey {
|
||||
r.mux.Lock()
|
||||
defer r.mux.Unlock()
|
||||
|
||||
if r.privateKey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := *r.privateKey
|
||||
return &c
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
)
|
||||
|
||||
type instancePrincipalConfigurationProvider struct {
|
||||
keyProvider *instancePrincipalKeyProvider
|
||||
region *common.Region
|
||||
}
|
||||
|
||||
//InstancePrincipalConfigurationProvider returns a configuration for instance principals
|
||||
func InstancePrincipalConfigurationProvider() (common.ConfigurationProvider, error) {
|
||||
var err error
|
||||
var keyProvider *instancePrincipalKeyProvider
|
||||
if keyProvider, err = newInstancePrincipalKeyProvider(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create a new key provider for instance principal: %s", err.Error())
|
||||
}
|
||||
return instancePrincipalConfigurationProvider{keyProvider: keyProvider, region: nil}, nil
|
||||
}
|
||||
|
||||
//InstancePrincipalConfigurationProviderForRegion returns a configuration for instance principals with a given region
|
||||
func InstancePrincipalConfigurationProviderForRegion(region common.Region) (common.ConfigurationProvider, error) {
|
||||
var err error
|
||||
var keyProvider *instancePrincipalKeyProvider
|
||||
if keyProvider, err = newInstancePrincipalKeyProvider(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create a new key provider for instance principal: %s", err.Error())
|
||||
}
|
||||
return instancePrincipalConfigurationProvider{keyProvider: keyProvider, region: ®ion}, nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) {
|
||||
return p.keyProvider.PrivateRSAKey()
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) KeyID() (string, error) {
|
||||
return p.keyProvider.KeyID()
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) TenancyOCID() (string, error) {
|
||||
return p.keyProvider.TenancyOCID()
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) UserOCID() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) KeyFingerprint() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p instancePrincipalConfigurationProvider) Region() (string, error) {
|
||||
if p.region == nil {
|
||||
return string(p.keyProvider.RegionForFederationClient()), nil
|
||||
}
|
||||
return string(*p.region), nil
|
||||
}
|
292
vendor/github.com/oracle/oci-go-sdk/common/auth/federation_client.go
generated
vendored
Normal file
292
vendor/github.com/oracle/oci-go-sdk/common/auth/federation_client.go
generated
vendored
Normal file
|
@ -0,0 +1,292 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
// Package auth provides supporting functions and structs for authentication
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// federationClient is a client to retrieve the security token for an instance principal necessary to sign a request.
|
||||
// It also provides the private key whose corresponding public key is used to retrieve the security token.
|
||||
type federationClient interface {
|
||||
PrivateKey() (*rsa.PrivateKey, error)
|
||||
SecurityToken() (string, error)
|
||||
}
|
||||
|
||||
// x509FederationClient retrieves a security token from Auth service.
|
||||
type x509FederationClient struct {
|
||||
tenancyID string
|
||||
sessionKeySupplier sessionKeySupplier
|
||||
leafCertificateRetriever x509CertificateRetriever
|
||||
intermediateCertificateRetrievers []x509CertificateRetriever
|
||||
securityToken securityToken
|
||||
authClient *common.BaseClient
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func newX509FederationClient(region common.Region, tenancyID string, leafCertificateRetriever x509CertificateRetriever, intermediateCertificateRetrievers []x509CertificateRetriever) federationClient {
|
||||
client := &x509FederationClient{
|
||||
tenancyID: tenancyID,
|
||||
leafCertificateRetriever: leafCertificateRetriever,
|
||||
intermediateCertificateRetrievers: intermediateCertificateRetrievers,
|
||||
}
|
||||
client.sessionKeySupplier = newSessionKeySupplier()
|
||||
client.authClient = newAuthClient(region, client)
|
||||
return client
|
||||
}
|
||||
|
||||
var (
|
||||
genericHeaders = []string{"date", "(request-target)"} // "host" is not needed for the federation endpoint. Don't ask me why.
|
||||
bodyHeaders = []string{"content-length", "content-type", "x-content-sha256"}
|
||||
)
|
||||
|
||||
func newAuthClient(region common.Region, provider common.KeyProvider) *common.BaseClient {
|
||||
signer := common.RequestSigner(provider, genericHeaders, bodyHeaders)
|
||||
client := common.DefaultBaseClientWithSigner(signer)
|
||||
if region == common.RegionSEA {
|
||||
client.Host = "https://auth.r1.oracleiaas.com"
|
||||
} else {
|
||||
client.Host = fmt.Sprintf(common.DefaultHostURLTemplate, "auth", string(region))
|
||||
}
|
||||
client.BasePath = "v1/x509"
|
||||
return &client
|
||||
}
|
||||
|
||||
// For authClient to sign requests to X509 Federation Endpoint
|
||||
func (c *x509FederationClient) KeyID() (string, error) {
|
||||
tenancy := c.tenancyID
|
||||
fingerprint := fingerprint(c.leafCertificateRetriever.Certificate())
|
||||
return fmt.Sprintf("%s/fed-x509/%s", tenancy, fingerprint), nil
|
||||
}
|
||||
|
||||
// For authClient to sign requests to X509 Federation Endpoint
|
||||
func (c *x509FederationClient) PrivateRSAKey() (*rsa.PrivateKey, error) {
|
||||
return c.leafCertificateRetriever.PrivateKey(), nil
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) PrivateKey() (*rsa.PrivateKey, error) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if err := c.renewSecurityTokenIfNotValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.sessionKeySupplier.PrivateKey(), nil
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) SecurityToken() (token string, err error) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if err = c.renewSecurityTokenIfNotValid(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.securityToken.String(), nil
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) renewSecurityTokenIfNotValid() (err error) {
|
||||
if c.securityToken == nil || !c.securityToken.Valid() {
|
||||
if err = c.renewSecurityToken(); err != nil {
|
||||
return fmt.Errorf("failed to renew security token: %s", err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) renewSecurityToken() (err error) {
|
||||
if err = c.sessionKeySupplier.Refresh(); err != nil {
|
||||
return fmt.Errorf("failed to refresh session key: %s", err.Error())
|
||||
}
|
||||
|
||||
if err = c.leafCertificateRetriever.Refresh(); err != nil {
|
||||
return fmt.Errorf("failed to refresh leaf certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
updatedTenancyID := extractTenancyIDFromCertificate(c.leafCertificateRetriever.Certificate())
|
||||
if c.tenancyID != updatedTenancyID {
|
||||
err = fmt.Errorf("unexpected update of tenancy OCID in the leaf certificate. Previous tenancy: %s, Updated: %s", c.tenancyID, updatedTenancyID)
|
||||
return
|
||||
}
|
||||
|
||||
for _, retriever := range c.intermediateCertificateRetrievers {
|
||||
if err = retriever.Refresh(); err != nil {
|
||||
return fmt.Errorf("failed to refresh intermediate certificate: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if c.securityToken, err = c.getSecurityToken(); err != nil {
|
||||
return fmt.Errorf("failed to get security token: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) getSecurityToken() (securityToken, error) {
|
||||
request := c.makeX509FederationRequest()
|
||||
|
||||
var err error
|
||||
var httpRequest http.Request
|
||||
if httpRequest, err = common.MakeDefaultHTTPRequestWithTaggedStruct(http.MethodPost, "", request); err != nil {
|
||||
return nil, fmt.Errorf("failed to make http request: %s", err.Error())
|
||||
}
|
||||
|
||||
var httpResponse *http.Response
|
||||
defer common.CloseBodyIfValid(httpResponse)
|
||||
if httpResponse, err = c.authClient.Call(context.Background(), &httpRequest); err != nil {
|
||||
return nil, fmt.Errorf("failed to call: %s", err.Error())
|
||||
}
|
||||
|
||||
response := x509FederationResponse{}
|
||||
if err = common.UnmarshalResponse(httpResponse, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal the response: %s", err.Error())
|
||||
}
|
||||
|
||||
return newInstancePrincipalToken(response.Token.Token)
|
||||
}
|
||||
|
||||
type x509FederationRequest struct {
|
||||
X509FederationDetails `contributesTo:"body"`
|
||||
}
|
||||
|
||||
// X509FederationDetails x509 federation details
|
||||
type X509FederationDetails struct {
|
||||
Certificate string `mandatory:"true" json:"certificate,omitempty"`
|
||||
PublicKey string `mandatory:"true" json:"publicKey,omitempty"`
|
||||
IntermediateCertificates []string `mandatory:"false" json:"intermediateCertificates,omitempty"`
|
||||
}
|
||||
|
||||
type x509FederationResponse struct {
|
||||
Token `presentIn:"body"`
|
||||
}
|
||||
|
||||
// Token token
|
||||
type Token struct {
|
||||
Token string `mandatory:"true" json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) makeX509FederationRequest() *x509FederationRequest {
|
||||
certificate := c.sanitizeCertificateString(string(c.leafCertificateRetriever.CertificatePemRaw()))
|
||||
publicKey := c.sanitizeCertificateString(string(c.sessionKeySupplier.PublicKeyPemRaw()))
|
||||
var intermediateCertificates []string
|
||||
for _, retriever := range c.intermediateCertificateRetrievers {
|
||||
intermediateCertificates = append(intermediateCertificates, c.sanitizeCertificateString(string(retriever.CertificatePemRaw())))
|
||||
}
|
||||
|
||||
details := X509FederationDetails{
|
||||
Certificate: certificate,
|
||||
PublicKey: publicKey,
|
||||
IntermediateCertificates: intermediateCertificates,
|
||||
}
|
||||
return &x509FederationRequest{details}
|
||||
}
|
||||
|
||||
func (c *x509FederationClient) sanitizeCertificateString(certString string) string {
|
||||
certString = strings.Replace(certString, "-----BEGIN CERTIFICATE-----", "", -1)
|
||||
certString = strings.Replace(certString, "-----END CERTIFICATE-----", "", -1)
|
||||
certString = strings.Replace(certString, "-----BEGIN PUBLIC KEY-----", "", -1)
|
||||
certString = strings.Replace(certString, "-----END PUBLIC KEY-----", "", -1)
|
||||
certString = strings.Replace(certString, "\n", "", -1)
|
||||
return certString
|
||||
}
|
||||
|
||||
// sessionKeySupplier provides an RSA keypair which can be re-generated by calling Refresh().
|
||||
type sessionKeySupplier interface {
|
||||
Refresh() error
|
||||
PrivateKey() *rsa.PrivateKey
|
||||
PublicKeyPemRaw() []byte
|
||||
}
|
||||
|
||||
// inMemorySessionKeySupplier implements sessionKeySupplier to vend an RSA keypair.
|
||||
// Refresh() generates a new RSA keypair with a random source, and keeps it in memory.
|
||||
//
|
||||
// inMemorySessionKeySupplier is not thread-safe.
|
||||
type inMemorySessionKeySupplier struct {
|
||||
keySize int
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKeyPemRaw []byte
|
||||
}
|
||||
|
||||
// newSessionKeySupplier creates and returns a sessionKeySupplier instance which generates key pairs of size 2048.
|
||||
func newSessionKeySupplier() sessionKeySupplier {
|
||||
return &inMemorySessionKeySupplier{keySize: 2048}
|
||||
}
|
||||
|
||||
// Refresh() is failure atomic, i.e., PrivateKey() and PublicKeyPemRaw() would return their previous values
|
||||
// if Refresh() fails.
|
||||
func (s *inMemorySessionKeySupplier) Refresh() (err error) {
|
||||
common.Debugln("Refreshing session key")
|
||||
|
||||
var privateKey *rsa.PrivateKey
|
||||
privateKey, err = rsa.GenerateKey(rand.Reader, s.keySize)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate a new keypair: %s", err)
|
||||
}
|
||||
|
||||
var publicKeyAsnBytes []byte
|
||||
if publicKeyAsnBytes, err = x509.MarshalPKIXPublicKey(privateKey.Public()); err != nil {
|
||||
return fmt.Errorf("failed to marshal the public part of the new keypair: %s", err.Error())
|
||||
}
|
||||
publicKeyPemRaw := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyAsnBytes,
|
||||
})
|
||||
|
||||
s.privateKey = privateKey
|
||||
s.publicKeyPemRaw = publicKeyPemRaw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inMemorySessionKeySupplier) PrivateKey() *rsa.PrivateKey {
|
||||
if s.privateKey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := *s.privateKey
|
||||
return &c
|
||||
}
|
||||
|
||||
func (s *inMemorySessionKeySupplier) PublicKeyPemRaw() []byte {
|
||||
if s.publicKeyPemRaw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := make([]byte, len(s.publicKeyPemRaw))
|
||||
copy(c, s.publicKeyPemRaw)
|
||||
return c
|
||||
}
|
||||
|
||||
type securityToken interface {
|
||||
fmt.Stringer
|
||||
Valid() bool
|
||||
}
|
||||
|
||||
type instancePrincipalToken struct {
|
||||
tokenString string
|
||||
jwtToken *jwtToken
|
||||
}
|
||||
|
||||
func newInstancePrincipalToken(tokenString string) (newToken securityToken, err error) {
|
||||
var jwtToken *jwtToken
|
||||
if jwtToken, err = parseJwt(tokenString); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse the token string \"%s\": %s", tokenString, err.Error())
|
||||
}
|
||||
return &instancePrincipalToken{tokenString, jwtToken}, nil
|
||||
}
|
||||
|
||||
func (t *instancePrincipalToken) String() string {
|
||||
return t.tokenString
|
||||
}
|
||||
|
||||
func (t *instancePrincipalToken) Valid() bool {
|
||||
return !t.jwtToken.expired()
|
||||
}
|
100
vendor/github.com/oracle/oci-go-sdk/common/auth/instance_principal_key_provider.go
generated
vendored
Normal file
100
vendor/github.com/oracle/oci-go-sdk/common/auth/instance_principal_key_provider.go
generated
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
)
|
||||
|
||||
const (
|
||||
regionURL = `http://169.254.169.254/opc/v1/instance/region`
|
||||
leafCertificateURL = `http://169.254.169.254/opc/v1/identity/cert.pem`
|
||||
leafCertificateKeyURL = `http://169.254.169.254/opc/v1/identity/key.pem`
|
||||
leafCertificateKeyPassphrase = `` // No passphrase for the private key for Compute instances
|
||||
intermediateCertificateURL = `http://169.254.169.254/opc/v1/identity/intermediate.pem`
|
||||
intermediateCertificateKeyURL = ``
|
||||
intermediateCertificateKeyPassphrase = `` // No passphrase for the private key for Compute instances
|
||||
)
|
||||
|
||||
// instancePrincipalKeyProvider implements KeyProvider to provide a key ID and its corresponding private key
|
||||
// for an instance principal by getting a security token via x509FederationClient.
|
||||
//
|
||||
// The region name of the endpoint for x509FederationClient is obtained from the metadata service on the compute
|
||||
// instance.
|
||||
type instancePrincipalKeyProvider struct {
|
||||
regionForFederationClient common.Region
|
||||
federationClient federationClient
|
||||
tenancyID string
|
||||
}
|
||||
|
||||
// newInstancePrincipalKeyProvider creates and returns an instancePrincipalKeyProvider instance based on
|
||||
// x509FederationClient.
|
||||
//
|
||||
// NOTE: There is a race condition between PrivateRSAKey() and KeyID(). These two pieces are tightly coupled; KeyID
|
||||
// includes a security token obtained from Auth service by giving a public key which is paired with PrivateRSAKey.
|
||||
// The x509FederationClient caches the security token in memory until it is expired. Thus, even if a client obtains a
|
||||
// KeyID that is not expired at the moment, the PrivateRSAKey that the client acquires at a next moment could be
|
||||
// invalid because the KeyID could be already expired.
|
||||
func newInstancePrincipalKeyProvider() (provider *instancePrincipalKeyProvider, err error) {
|
||||
var region common.Region
|
||||
if region, err = getRegionForFederationClient(regionURL); err != nil {
|
||||
err = fmt.Errorf("failed to get the region name from %s: %s", regionURL, err.Error())
|
||||
common.Logln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leafCertificateRetriever := newURLBasedX509CertificateRetriever(
|
||||
leafCertificateURL, leafCertificateKeyURL, leafCertificateKeyPassphrase)
|
||||
intermediateCertificateRetrievers := []x509CertificateRetriever{
|
||||
newURLBasedX509CertificateRetriever(
|
||||
intermediateCertificateURL, intermediateCertificateKeyURL, intermediateCertificateKeyPassphrase),
|
||||
}
|
||||
|
||||
if err = leafCertificateRetriever.Refresh(); err != nil {
|
||||
err = fmt.Errorf("failed to refresh the leaf certificate: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
tenancyID := extractTenancyIDFromCertificate(leafCertificateRetriever.Certificate())
|
||||
|
||||
federationClient := newX509FederationClient(
|
||||
region, tenancyID, leafCertificateRetriever, intermediateCertificateRetrievers)
|
||||
|
||||
provider = &instancePrincipalKeyProvider{regionForFederationClient: region, federationClient: federationClient, tenancyID: tenancyID}
|
||||
return
|
||||
}
|
||||
|
||||
func getRegionForFederationClient(url string) (r common.Region, err error) {
|
||||
var body bytes.Buffer
|
||||
if body, err = httpGet(url); err != nil {
|
||||
return
|
||||
}
|
||||
return common.StringToRegion(body.String()), nil
|
||||
}
|
||||
|
||||
func (p *instancePrincipalKeyProvider) RegionForFederationClient() common.Region {
|
||||
return p.regionForFederationClient
|
||||
}
|
||||
|
||||
func (p *instancePrincipalKeyProvider) PrivateRSAKey() (privateKey *rsa.PrivateKey, err error) {
|
||||
if privateKey, err = p.federationClient.PrivateKey(); err != nil {
|
||||
err = fmt.Errorf("failed to get private key: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func (p *instancePrincipalKeyProvider) KeyID() (string, error) {
|
||||
var securityToken string
|
||||
var err error
|
||||
if securityToken, err = p.federationClient.SecurityToken(); err != nil {
|
||||
return "", fmt.Errorf("failed to get security token: %s", err.Error())
|
||||
}
|
||||
return fmt.Sprintf("ST$%s", securityToken), nil
|
||||
}
|
||||
|
||||
func (p *instancePrincipalKeyProvider) TenancyOCID() (string, error) {
|
||||
return p.tenancyID, nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jwtToken struct {
|
||||
raw string
|
||||
header map[string]interface{}
|
||||
payload map[string]interface{}
|
||||
}
|
||||
|
||||
func (t *jwtToken) expired() bool {
|
||||
exp := int64(t.payload["exp"].(float64))
|
||||
return exp <= time.Now().Unix()
|
||||
}
|
||||
|
||||
func parseJwt(tokenString string) (*jwtToken, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("the given token string contains an invalid number of parts")
|
||||
}
|
||||
|
||||
token := &jwtToken{raw: tokenString}
|
||||
var err error
|
||||
|
||||
// Parse Header part
|
||||
var headerBytes []byte
|
||||
if headerBytes, err = decodePart(parts[0]); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode the header bytes: %s", err.Error())
|
||||
}
|
||||
if err = json.Unmarshal(headerBytes, &token.header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse Payload part
|
||||
var payloadBytes []byte
|
||||
if payloadBytes, err = decodePart(parts[1]); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode the payload bytes: %s", err.Error())
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewBuffer(payloadBytes))
|
||||
if err = decoder.Decode(&token.payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode the payload json: %s", err.Error())
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func decodePart(partString string) ([]byte, error) {
|
||||
if l := len(partString) % 4; 0 < l {
|
||||
partString += strings.Repeat("=", 4-l)
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(partString)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/common"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// httpGet makes a simple HTTP GET request to the given URL, expecting only "200 OK" status code.
|
||||
// This is basically for the Instance Metadata Service.
|
||||
func httpGet(url string) (body bytes.Buffer, err error) {
|
||||
var response *http.Response
|
||||
if response, err = http.Get(url); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
common.IfDebug(func() {
|
||||
if dump, e := httputil.DumpResponse(response, true); e == nil {
|
||||
common.Logf("Dump Response %v", string(dump))
|
||||
} else {
|
||||
common.Debugln(e)
|
||||
}
|
||||
})
|
||||
|
||||
defer response.Body.Close()
|
||||
if _, err = body.ReadFrom(response.Body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("HTTP Get failed: URL: %s, Status: %s, Message: %s",
|
||||
url, response.Status, body.String())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func extractTenancyIDFromCertificate(cert *x509.Certificate) string {
|
||||
for _, nameAttr := range cert.Subject.Names {
|
||||
value := nameAttr.Value.(string)
|
||||
if strings.HasPrefix(value, "opc-tenant:") {
|
||||
return value[len("opc-tenant:"):]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fingerprint(certificate *x509.Certificate) string {
|
||||
fingerprint := sha1.Sum(certificate.Raw)
|
||||
return colonSeparatedString(fingerprint)
|
||||
}
|
||||
|
||||
func colonSeparatedString(fingerprint [sha1.Size]byte) string {
|
||||
spaceSeparated := fmt.Sprintf("% x", fingerprint)
|
||||
return strings.Replace(spaceSeparated, " ", ":", -1)
|
||||
}
|
|
@ -484,6 +484,7 @@ github.com/nu7hatch/gouuid
|
|||
github.com/olekukonko/tablewriter
|
||||
# github.com/oracle/oci-go-sdk v1.8.0
|
||||
github.com/oracle/oci-go-sdk/common
|
||||
github.com/oracle/oci-go-sdk/common/auth
|
||||
github.com/oracle/oci-go-sdk/core
|
||||
# github.com/outscale/osc-go v0.0.1
|
||||
github.com/outscale/osc-go/oapi
|
||||
|
|
Loading…
Reference in New Issue