Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCI Provider private zone and workload identity support #3995

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/tutorials/oracle.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ in which you created the zone; you'll need to provide that later.

For more information about OCI DNS see the documentation [here][1].

## Using Private OCI DNS Zones

By default, the ExternalDNS OCI provider is configured to use Global OCI
DNS Zones. If you want to use Private OCI DNS Zones, add the following
argument to the ExternalDNS controller:

```
--oci-zone-scope=PRIVATE
```

To use both Global and Private OCI DNS Zones, set the OCI Zone Scope to be
empty:

```
--oci-zone-scope=
```

## Deploy ExternalDNS

Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Expand Down Expand Up @@ -68,6 +85,35 @@ 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].

### OCI IAM Workload Identity

If you're running ExternalDNS within an OCI Container Engine for Kubernetes (OKE) cluster,
you can use OCI IAM Workload Identity to authenticate with OCI. 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 covering your OKE cluster running ExternalDNS.
E.g.:

```
Allow any-user to manage dns in compartment <compartment-name> where all {request.principal.type='workload',request.principal.cluster_id='<cluster-ocid>',request.principal.service_account='external-dns'}
```

You'll also need to create a new file (oci.yaml) and modify the contents to match the example
below. Be sure to adjust the values to match your region and the OCID
of the compartment containing the zone:

```yaml
auth:
region: us-phoenix-1
useWorkloadIdentity: true
compartment: ocid1.compartment.oc1...
```

Create a secret using the config file above:

```shell
$ kubectl create secret generic external-dns-config --from-file=oci.yaml
```

## Manifest (for clusters with RBAC enabled)

Apply the following manifest to deploy ExternalDNS.
Expand Down Expand Up @@ -131,6 +177,9 @@ spec:
- --provider=oci
- --policy=upsert-only # prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --txt-owner-id=my-identifier
# Specifies the OCI DNS Zone scope, defaults to GLOBAL.
# May be GLOBAL, PRIVATE, or an empty value to specify both GLOBAL and PRIVATE OCI DNS Zones
# - --oci-zone-scope=GLOBAL
volumeMounts:
- name: config
mountPath: /etc/kubernetes/
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ func main() {
}

if err == nil {
p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.OCIZoneScope, cfg.DryRun)
}
case "rfc2136":
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, nil)
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type Config struct {
OCIConfigFile string
OCICompartmentOCID string
OCIAuthInstancePrincipal bool
OCIZoneScope string
InMemoryZones []string
OVHEndpoint string
OVHApiRateLimit int
Expand Down Expand Up @@ -291,6 +292,7 @@ var defaultConfig = &Config{
InfobloxCreatePTR: false,
InfobloxCacheDuration: 0,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
OCIZoneScope: "GLOBAL",
InMemoryZones: []string{},
OVHEndpoint: "ovh-eu",
OVHApiRateLimit: 20,
Expand Down Expand Up @@ -523,6 +525,7 @@ func (cfg *Config) ParseFlags(args []string) error {
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-zone-scope", "When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both.").Default(defaultConfig.OCIZoneScope).EnumVar(&cfg.OCIZoneScope, "", "GLOBAL", "PRIVATE")
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)
Expand Down
6 changes: 5 additions & 1 deletion pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var (
APIServerURL: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
GlooNamespaces: []string{"gloo-system"},
GlooNamespaces: []string{"gloo-system"},
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: []string{"service"},
Namespace: "",
Expand Down Expand Up @@ -94,6 +94,7 @@ var (
InfobloxSSLVerify: true,
InfobloxMaxResults: 0,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
OCIZoneScope: "GLOBAL",
InMemoryZones: []string{""},
OVHEndpoint: "ovh-eu",
OVHApiRateLimit: 20,
Expand Down Expand Up @@ -203,6 +204,7 @@ var (
InfobloxSSLVerify: false,
InfobloxMaxResults: 2000,
OCIConfigFile: "oci.yaml",
OCIZoneScope: "PRIVATE",
InMemoryZones: []string{"example.org", "company.com"},
OVHEndpoint: "ovh-ca",
OVHApiRateLimit: 42,
Expand Down Expand Up @@ -325,6 +327,7 @@ func TestParseFlags(t *testing.T) {
"--pdns-api-key=some-secret-key",
"--pdns-skip-tls-verify",
"--oci-config-file=oci.yaml",
"--oci-zone-scope=PRIVATE",
"--tls-ca=/path/to/ca.crt",
"--tls-client-cert=/path/to/cert.pem",
"--tls-client-cert-key=/path/to/key.pem",
Expand Down Expand Up @@ -445,6 +448,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_OCI_ZONE_SCOPE": "PRIVATE",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_OVH_ENDPOINT": "ovh-ca",
"EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42",
Expand Down
57 changes: 43 additions & 14 deletions provider/oci/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type OCIAuthConfig struct {
Fingerprint string `yaml:"fingerprint"`
Passphrase string `yaml:"passphrase"`
UseInstancePrincipal bool `yaml:"useInstancePrincipal"`
UseWorkloadIdentity bool `yaml:"useWorkloadIdentity"`
}

// OCIConfig holds the configuration for the OCI Provider.
Expand All @@ -61,6 +62,7 @@ type OCIProvider struct {

domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
zoneScope string
dryRun bool
}

Expand All @@ -87,11 +89,26 @@ 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) {
func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneScope string, dryRun bool) (*OCIProvider, error) {
var client ociDNSClient
var err error
var configProvider common.ConfigurationProvider
if cfg.Auth.UseInstancePrincipal {
if cfg.Auth.UseInstancePrincipal && cfg.Auth.UseWorkloadIdentity {
return nil, errors.New("only one of 'useInstancePrincipal' and 'useWorkloadIdentity' may be enabled for Oracle authentication")
}
if cfg.Auth.UseWorkloadIdentity {
// OCI SDK requires specific, dynamic environment variables for workload identity.
if err := os.Setenv(auth.ResourcePrincipalVersionEnvVar, auth.ResourcePrincipalVersion2_2); err != nil {
return nil, errors.Wrapf(err, "unable to set OCI SDK environment variable: %s", auth.ResourcePrincipalVersionEnvVar)
}
if err := os.Setenv(auth.ResourcePrincipalRegionEnvVar, cfg.Auth.Region); err != nil {
return nil, errors.Wrapf(err, "unable to set OCI SDK environment variable: %s", auth.ResourcePrincipalRegionEnvVar)
}
configProvider, err = auth.OkeWorkloadIdentityConfigurationProvider()
if err != nil {
return nil, errors.Wrap(err, "error creating OCI workload identity config provider")
}
} else if cfg.Auth.UseInstancePrincipal {
configProvider, err = auth.InstancePrincipalConfigurationProvider()
if err != nil {
return nil, errors.Wrap(err, "error creating OCI instance principal config provider")
Expand All @@ -117,44 +134,56 @@ func NewOCIProvider(cfg OCIConfig, domainFilter endpoint.DomainFilter, zoneIDFil
cfg: cfg,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
zoneScope: zoneScope,
dryRun: dryRun,
}, nil
}

func (p *OCIProvider) zones(ctx context.Context) (map[string]dns.ZoneSummary, error) {
zones := make(map[string]dns.ZoneSummary)

scopes := []dns.GetZoneScopeEnum{dns.GetZoneScopeEnum(p.zoneScope)}
// If zone scope is empty, list all zones types.
if p.zoneScope == "" {
scopes = dns.GetGetZoneScopeEnumValues()
}
log.Debugf("Matching zones against domain filters: %v", p.domainFilter.Filters)
for _, scope := range scopes {
if err := p.addPaginatedZones(ctx, zones, scope); err != nil {
return nil, err
}
}
if len(zones) == 0 {
log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter)
}
return zones, nil
}

func (p *OCIProvider) addPaginatedZones(ctx context.Context, zones map[string]dns.ZoneSummary, scope dns.GetZoneScopeEnum) error {
var page *string
// Loop until we have listed all zones.
for {
resp, err := p.client.ListZones(ctx, dns.ListZonesRequest{
CompartmentId: &p.cfg.CompartmentID,
ZoneType: dns.ListZonesZoneTypePrimary,
Scope: dns.ListZonesScopeEnum(scope),
Page: page,
})
if err != nil {
return nil, errors.Wrapf(err, "listing zones in %q", p.cfg.CompartmentID)
return errors.Wrapf(err, "listing zones in %s", p.cfg.CompartmentID)
}

for _, zone := range resp.Items {
if p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.Id) {
zones[*zone.Name] = zone
zones[*zone.Id] = zone
log.Debugf("Matched %q (%q)", *zone.Name, *zone.Id)
} else {
log.Debugf("Filtered %q (%q)", *zone.Name, *zone.Id)
}
}

if page = resp.OpcNextPage; resp.OpcNextPage == nil {
break
}
}

if len(zones) == 0 {
log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter)
}

return zones, nil
return nil
}

func (p *OCIProvider) newFilteredRecordOperations(endpoints []*endpoint.Endpoint, opType dns.RecordOperationOperationEnum) []dns.RecordOperation {
Expand Down Expand Up @@ -261,7 +290,7 @@ func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
// newRecordOperation returns a RecordOperation based on a given endpoint.
func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperationEnum) dns.RecordOperation {
targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets))
copy(targets, ep.Targets)
if ep.RecordType == endpoint.RecordTypeCNAME {
targets[0] = provider.EnsureTrailingDot(targets[0])
}
Expand Down
Loading
Loading