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

add cname records support #495 #498

Merged
merged 3 commits into from
Nov 10, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
./gradlew ci publish -PrepoUser=${{ secrets.ARTIFACTORY_USERNAME }} -PrepoPassword=${{ secrets.ARTIFACTORY_PASSWORD }}
else
echo "Building release version"
./gradlew ci
./gradlew build
fi

- name: Codecov
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version = 0.21.5
version = 0.22.0

2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ fabric8-kubernetes-clientapi = { module = "io.fabric8:kubernetes-client-api", ve
fabric8-kubernetes-client = { module = "io.fabric8:kubernetes-client", version = "6.6.0" }
fabric8-kubernetes-servermock = { module = "io.fabric8:kubernetes-server-mock", version = "6.6.2" }
mockito-core = { module = "org.mockito:mockito-core", version = "5.4.0" }
mockito-inline = { module = "org.mockito:mockito-inline", version = "5.4.0" }
mockito-junitjupiter = { module = "org.mockito:mockito-junit-jupiter", version = "5.4.0" }

okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version = "3.12.2" }
okhttp3-okhttpsse = { module = "com.squareup.okhttp3:okhttp-sse", version = "3.12.2" }
Expand Down
4 changes: 4 additions & 0 deletions java-operator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ dependencies {
testImplementation "io.quarkus:quarkus-junit5"
testImplementation "io.rest-assured:rest-assured"
testImplementation "io.quarkus:quarkus-jacoco"
testImplementation libs.fabric8.kubernetes.servermock
testImplementation libs.mockito.inline
testImplementation libs.mockito.junitjupiter

testCompileOnly libs.immutables.valueannotations
testAnnotationProcessor libs.immutables.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,30 @@

import com.enonic.kubernetes.client.DefaultEnonicKubernetesClient;
import com.enonic.kubernetes.client.EnonicKubernetesClient;

import io.fabric8.kubernetes.client.DefaultKubernetesClient;

import io.fabric8.kubernetes.client.NamespacedKubernetesClient;
import io.quarkus.arc.profile.IfBuildProfile;
import io.quarkus.arc.profile.UnlessBuildProfile;
import io.quarkus.runtime.configuration.ProfileManager;

import static com.enonic.kubernetes.common.SingletonAssert.singletonAssert;

public class ClientsProducer
{
@Singleton
@Produces
@IfBuildProfile("prod")
Clients createClients()
{
singletonAssert(this, "createClients");
NamespacedKubernetesClient defaultKubernetesClient = new DefaultKubernetesClient().inAnyNamespace();
EnonicKubernetesClient client = new DefaultEnonicKubernetesClient(defaultKubernetesClient);

ProfileManager.getActiveProfile();

final NamespacedKubernetesClient defaultKubernetesClient = new DefaultKubernetesClient().inAnyNamespace();
final EnonicKubernetesClient client = new DefaultEnonicKubernetesClient(defaultKubernetesClient);

return ClientsImpl.of(
client.k8s(),
client.enonic(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,24 @@ private void domain( final MutationRequest mt )
Domain newR = (Domain) mt.getAdmissionReview().getRequest().getObject();

// Create default status
DomainStatus defStatus = new DomainStatus().
withState( DomainStatus.State.PENDING ).
withMessage( "Waiting for DNS records" ).
withDomainStatusFields( new DomainStatusFields( lbServiceIpProducer.get(), false ) );
DomainStatus defStatus;
try
{
final DomainStatusFields domainStatusFields;
domainStatusFields = new DomainStatusFields( lbServiceIpProducer.get(), false );

defStatus = new DomainStatus().
withState( DomainStatus.State.PENDING ).
withMessage( "Waiting for DNS records" ).
withDomainStatusFields( domainStatusFields );
}
catch ( Exception e )
{
defStatus = new DomainStatus().
withState( DomainStatus.State.ERROR ).
withMessage( e.getMessage() ).
withDomainStatusFields( new DomainStatusFields(List.of(), false) );
}

// Get OP
AdmissionOperation op = getOperation( mt.getAdmissionReview() );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.enonic.kubernetes.operator.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.enonic.kubernetes.apis.cloudflare.DnsRecordServiceWrapper;
import com.enonic.kubernetes.apis.cloudflare.service.model.DnsRecord;
import com.enonic.kubernetes.apis.cloudflare.service.model.ImmutableDnsRecord;
import com.enonic.kubernetes.client.v1.domain.Domain;

public final class DnsRecordManager
{
private final Logger LOG = LoggerFactory.getLogger( DnsRecordManager.class );

private final DnsRecordServiceWrapper dnsRecordService;

private final DomainConfig config;

private final Domain domain;

protected DnsRecordManager( DnsRecordServiceWrapper dnsRecordService, DomainConfig config,
Domain domain )
{
this.dnsRecordService = dnsRecordService;
this.config = config;
this.domain = domain;
}

public String syncRecords( final List<DnsRecord> allRecords, final List<String> lbAddresses, final String type, final String clusterId )
{
final List<DnsRecord> typedRecords = allRecords.stream().filter( r -> type.equals( r.type() ) ).collect( Collectors.toList() );
final List<String> currentRecordAddresses = typedRecords.stream().map( DnsRecord::content ).collect( Collectors.toList() );

if ( getHeritageRecord( allRecords, clusterId ) == null )
{
final String heritageRecordMsg = this.addHeritageRecord( clusterId );
if ( heritageRecordMsg != null )
{
return heritageRecordMsg;
}
}

final List<String> errorMessages = new ArrayList<>();

// Remove all records that do not have the current address(ip or cname) the lb has
final List<DnsRecord> recordsToDelete = allRecords.stream()
.filter( record -> "A".equals( record.type() ) || "CNAME".equals( record.type() ) )
.filter( record -> !lbAddresses.contains( record.content() ) )
.collect( Collectors.toList() );

recordsToDelete.stream().map( this::deleteRecord ).forEach( errorMessages::add );

lbAddresses.stream()
.filter( lbAddress -> !currentRecordAddresses.contains( lbAddress ) )
.map( ip -> this.addRecord( ImmutableDnsRecord.builder()
.zone_id( config.zoneId() )
.name( domain.getSpec().getHost() )
.ttl( domain.getSpec().getDnsTTL() )
.content( ip )
.type( type )
.proxied( domain.getSpec().getCdn() )
.build() ) )
.forEach( errorMessages::add );

// Update existing records if needed
typedRecords.stream()
.filter( r -> !recordsToDelete.contains( r ) )
.filter( r -> !Objects.equals( r.ttl(), domain.getSpec().getDnsTTL() ) || r.proxied() != domain.getSpec().getCdn() )
.map( r -> this.updateRecord(
ImmutableDnsRecord.builder().from( r ).ttl( domain.getSpec().getDnsTTL() ).proxied( domain.getSpec().getCdn() ).build() ) )
.forEach( errorMessages::add );

return errorMessages.stream().filter( Objects::nonNull ).collect( Collectors.joining( "," ) );
}

public String deleteRecords( final List<DnsRecord> records )
{
return records.stream().map( this::deleteRecord ).filter( Objects::nonNull ).collect( Collectors.joining( "," ) );
}

public DnsRecord getHeritageRecord( final List<DnsRecord> records, final String clusterId )
{
// Get heritage record
return records.stream()
.filter( r -> "TXT".equals( r.type() ) )
.filter( r -> getHeritageRecordContent( clusterId ).equals( r.content() ) )
.findFirst()
.orElse( null );
}


private String deleteRecord( DnsRecord record )
{
return handleDnsOperation( record, dnsRecordService::delete, "delete" );
}

private String updateRecord( DnsRecord record )
{
return handleDnsOperation( record, dnsRecordService::update, "update" );
}

private String addRecord( DnsRecord record )
{
return handleDnsOperation( record, dnsRecordService::create, "create" );
}

private String addHeritageRecord( final String clusterId )
{
return handleDnsOperation( ImmutableDnsRecord.builder()
.zone_id( config.zoneId() )
.name( domain.getSpec().getHost() )
.ttl( domain.getSpec().getDnsTTL() )
.type( "TXT" )
.content( getHeritageRecordContent( clusterId ) )
.build(), dnsRecordService::create, "create heritage" );
}

private String getHeritageRecordContent( final String clusterId )
{
return "heritage=xp-operator,id=" + clusterId;
}


private String handleDnsOperation( final DnsRecord record, final Function<DnsRecord, Runnable> operation,
final String operationDescription )
{
try
{
operation.apply( record ).run();
}
catch ( Exception e )
{
final String errorMessage =
String.format( "Failed to %s record: %s with type: %s and content: %s due to: %s.", operationDescription, record.name(),
record.type(), record.content(), e.getMessage() );

LOG.error( errorMessage, e );

return errorMessage;
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.enonic.kubernetes.operator.domain;


import java.util.Optional;

import javax.enterprise.inject.Default;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;

@ConfigMapping(prefix = "dns.lb")
public interface LBConfig
{
@WithName("cname")
Optional<String> cname();

@WithName("staticIp")
Optional<String> staticIp();

@WithName("service.name")
String serviceName();

@WithName("service.namespace")
String serviceNamespace();

@WithName("domain.wildcardCertificate")
String domainWildcardCertificate();

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.enonic.kubernetes.operator.domain;

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

Expand All @@ -16,65 +16,79 @@

import com.enonic.kubernetes.kubernetes.Clients;

import static com.enonic.kubernetes.common.Configuration.cfgHasKey;
import static com.enonic.kubernetes.common.Configuration.cfgStr;
import static com.enonic.kubernetes.common.SingletonAssert.singletonAssert;

@Singleton
public class LbServiceIpProducer
implements Supplier<List<String>>
{
private final Logger log = LoggerFactory.getLogger( LbServiceIpProducer.class );
private final Logger LOG = LoggerFactory.getLogger( LbServiceIpProducer.class );

@Inject
Clients clients;

@Inject
LBConfig lbConfig;

@ConfigProperty(name = "dns.enabled")
boolean dnsEnabled;


public LbServiceIpProducer()
{
singletonAssert(this, "constructor");
singletonAssert( this, "constructor" );
}

private List<String> getLbIp( final Clients clients )
{
List<String> res = new LinkedList<>();
if ( cfgHasKey( "dns.lb.staticIp" ) )

if ( lbConfig.cname().isPresent() )
{
if ( lbConfig.staticIp().isPresent() )
{
throw new RuntimeException( "Both dns.lb.staticIp and dns.lb.cname are set, only one of them can be set" );
}
else
{
LOG.info( String.format( "Loadbalancer CNAME is %s", lbConfig.cname().get() ) );

return List.of();
}
}
else if ( lbConfig.staticIp().isPresent() )
{
res.add( cfgStr( "dns.lb.staticIp" ) );
LOG.info( String.format( "Loadbalancer static IP is %s", lbConfig.staticIp().get() ) );

return List.of( lbConfig.staticIp().get() );
}

if ( res.isEmpty() )
final Service lbService =
clients.k8s().services().inNamespace( lbConfig.serviceNamespace() ).withName( lbConfig.serviceName() ).get();

if ( lbService == null && dnsEnabled )
{
Service lbService = clients.k8s().services().
inNamespace( cfgStr( "dns.lb.service.namespace" ) ).
withName( cfgStr( "dns.lb.service.name" ) ).
get();
LOG.warn( "Loadbalancer service not found" );
}

if ( lbService == null && dnsEnabled )
{
log.warn( "Loadbalancer service not found" );
}
final List<String> res = new ArrayList<>();

if ( lbService != null && lbService.getStatus() != null && lbService.getStatus().getLoadBalancer() != null &&
lbService.getStatus().getLoadBalancer().getIngress() != null &&
lbService.getStatus().getLoadBalancer().getIngress().size() == 1 )
if ( lbService != null && lbService.getStatus() != null && lbService.getStatus().getLoadBalancer() != null &&
lbService.getStatus().getLoadBalancer().getIngress() != null &&
lbService.getStatus().getLoadBalancer().getIngress().size() == 1 )
{
for ( LoadBalancerIngress ingress : lbService.getStatus().getLoadBalancer().getIngress() )
{
for ( LoadBalancerIngress ingress : lbService.getStatus().getLoadBalancer().getIngress() )
{
res.add( ingress.getIp() );
}
res.add( ingress.getIp() );
}
}

if ( !res.isEmpty() )
{
log.info( String.format( "Loadbalancer IPs are %s", res ) );
LOG.info( String.format( "Loadbalancer IPs are %s", res ) );
}
else if ( dnsEnabled )
{
log.warn( "Loadbalancer ip not found, domain DNS records wont work" );
LOG.warn( "Loadbalancer ip not found, domain DNS records wont work" );
}

return res;
Expand All @@ -85,4 +99,5 @@ public List<String> get()
{
return getLbIp( clients );
}

}
Loading