diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index f5b0a168c..2a204697e 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -6,16 +6,25 @@ Make sure to use the latest version of ExternalDNS for this tutorial. ## Creating an OCI DNS Zone -Create a DNS zone which will contain the managed DNS records. Let's use `example.com` as an reference here. +Create a DNS zone which will contain the managed DNS records. Let's use +`example.com` as a reference here. Make note of the OCID of the compartment +in which you created the zone; you'll need to provide that later. For more information about OCI DNS see the documentation [here][1]. ## Deploy ExternalDNS Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +The OCI provider supports two authentication options: key-based and instance +principals. + +### Key-based + We first need to create a config file containing the information needed to connect with the OCI API. -Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials: +Create a new file (oci.yaml) and modify the contents to match the example +below. Be sure to adjust the values to match your own credentials, and the OCID +of the compartment containing the zone: ```yaml auth: @@ -37,7 +46,29 @@ Create a secret using the config file above: $ kubectl create secret generic external-dns-config --from-file=oci.yaml ``` -### Manifest (for clusters with RBAC enabled) +### OCI IAM Instance Principal + +If you're running ExternalDNS within OCI, you can use OCI IAM instance +principals to authenticate with OCI. This obviates the need to create the +secret with your credentials. You'll need to ensure an OCI IAM policy exists +with a statement granting the `manage dns` permission on zones and records in +the target compartment to the dynamic group covering your instance running +ExternalDNS. +E.g.: + +``` +Allow dynamic-group to manage dns in compartment id +``` + +You'll also need to add the `--oci-instance-principals=true` flag to enable +this type of authentication. Finally, you'll need to add the +`--oci-compartment-ocid=ocid1.compartment.oc1...` flag to provide the OCID of +the compartment containing the zone to be managed. + +For more information about OCI IAM instance principals, see the documentation [here][2]. +For more information about OCI IAM policy details for the DNS service, see the documentation [here][3]. + +## Manifest (for clusters with RBAC enabled) Apply the following manifest to deploy ExternalDNS. @@ -159,3 +190,6 @@ $ kubectl apply -f nginx.yaml ``` [1]: https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm +[2]: https://docs.cloud.oracle.com/iaas/Content/Identity/Reference/dnspolicyreference.htm +[3]: https://docs.cloud.oracle.com/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm + diff --git a/main.go b/main.go index ece1b0290..5b34b7ba7 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -312,7 +313,19 @@ func main() { ) case "oci": var config *oci.OCIConfig - config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + // if the instance-principals flag was set, and a compartment OCID was provided, then ignore the + // OCI config file, and provide a config that uses instance principal authentication. + if cfg.OCIAuthInstancePrincipal { + if len(cfg.OCICompartmentOCID) == 0 { + err = fmt.Errorf("instance principal authentication requested, but no compartment OCID provided") + } else { + authConfig := oci.OCIAuthConfig{UseInstancePrincipal: true} + config = &oci.OCIConfig{Auth: authConfig, CompartmentID: cfg.OCICompartmentOCID} + } + } else { + config, err = oci.LoadOCIConfig(cfg.OCIConfigFile) + } + if err == nil { p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 1eb8570bf..de5d7fc56 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -131,6 +131,8 @@ type Config struct { DynPassword string `secure:"yes"` DynMinTTLSeconds int OCIConfigFile string + OCICompartmentOCID string + OCIAuthInstancePrincipal bool InMemoryZones []string OVHEndpoint string OVHApiRateLimit int @@ -498,6 +500,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword) app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds) app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile) + app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records. Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID) + app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal) app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt) app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones) app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint) diff --git a/provider/oci/oci.go b/provider/oci/oci.go index f9a47a48b..06e9e8a3b 100644 --- a/provider/oci/oci.go +++ b/provider/oci/oci.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/oracle/oci-go-sdk/common" + "github.com/oracle/oci-go-sdk/common/auth" "github.com/oracle/oci-go-sdk/dns" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -36,12 +37,13 @@ const ociRecordTTL = 300 // OCIAuthConfig holds connection parameters for the OCI API. type OCIAuthConfig struct { - Region string `yaml:"region"` - TenancyID string `yaml:"tenancy"` - UserID string `yaml:"user"` - PrivateKey string `yaml:"key"` - Fingerprint string `yaml:"fingerprint"` - Passphrase string `yaml:"passphrase"` + Region string `yaml:"region"` + TenancyID string `yaml:"tenancy"` + UserID string `yaml:"user"` + PrivateKey string `yaml:"key"` + Fingerprint string `yaml:"fingerprint"` + Passphrase string `yaml:"passphrase"` + UseInstancePrincipal bool `yaml:"useInstancePrincipal"` } // OCIConfig holds the configuration for the OCI Provider. @@ -87,14 +89,25 @@ func LoadOCIConfig(path string) (*OCIConfig, error) { // NewOCIProvider initializes a new OCI DNS based Provider. func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (*OCIProvider, error) { var client ociDNSClient - client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider( - cfg.Auth.TenancyID, - cfg.Auth.UserID, - cfg.Auth.Region, - cfg.Auth.Fingerprint, - cfg.Auth.PrivateKey, - &cfg.Auth.Passphrase, - )) + var err error + var configProvider common.ConfigurationProvider + if cfg.Auth.UseInstancePrincipal { + configProvider, err = auth.InstancePrincipalConfigurationProvider() + if err != nil { + return nil, errors.Wrap(err, "error creating OCI instance principal config provider") + } + } else { + configProvider = common.NewRawConfigurationProvider( + cfg.Auth.TenancyID, + cfg.Auth.UserID, + cfg.Auth.Region, + cfg.Auth.Fingerprint, + cfg.Auth.PrivateKey, + &cfg.Auth.Passphrase, + ) + } + + client, err = dns.NewDnsClientWithConfigurationProvider(configProvider) if err != nil { return nil, errors.Wrap(err, "initializing OCI DNS API client") } diff --git a/provider/oci/oci_test.go b/provider/oci/oci_test.go index 01bed1d97..33ca5c244 100644 --- a/provider/oci/oci_test.go +++ b/provider/oci/oci_test.go @@ -19,6 +19,7 @@ package oci import ( "context" "sort" + "strings" "testing" "github.com/oracle/oci-go-sdk/common" @@ -167,6 +168,17 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K }, }, }, + "instance-principal": { + // testing the InstancePrincipalConfigurationProvider is tricky outside of an OCI context, because it tries + // to request a token from the internal OCI systems; this test-case just confirms that the expected error is + // observed, confirming that the instance-principal provider was instantiated. + config: OCIConfig{ + Auth: OCIAuthConfig{ + UseInstancePrincipal: true, + }, + }, + err: errors.New("error creating OCI instance principal config provider: failed to create a new key provider for instance principal"), + }, "invalid": { config: OCIConfig{ Auth: OCIAuthConfig{ @@ -192,7 +204,8 @@ hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K if err == nil { require.NoError(t, err) } else { - require.Equal(t, tc.err.Error(), err.Error()) + // have to use prefix testing because the expected instance-principal error strings vary after a known prefix + require.Truef(t, strings.HasPrefix(err.Error(), tc.err.Error()), "observed: %s", err.Error()) } }) }