ApplicationServiceTopologyConfiguration.java

package com.cloudforgeci.api.core.topology;

import com.cloudforgeci.api.core.SystemContext;
import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.enums.TopologyType;
import com.cloudforgeci.api.interfaces.TopologyConfiguration;
import com.cloudforgeci.api.interfaces.Rule;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.applicationautoscaling.EnableScalingProps;
import software.amazon.awscdk.services.ecs.CpuUtilizationScalingProps;
import software.amazon.awscdk.services.ecs.ScalableTaskCount;
import software.amazon.awscdk.services.route53.ARecord;
import software.amazon.awscdk.services.route53.AaaaRecord;
import software.amazon.awscdk.services.route53.ARecordProps;
import software.amazon.awscdk.services.route53.AaaaRecordProps;
import software.amazon.awscdk.services.route53.RecordTarget;
import software.amazon.awscdk.services.route53.targets.LoadBalancerTarget;

import java.util.logging.Logger;

import java.util.ArrayList;
import java.util.List;

import static com.cloudforgeci.api.core.rules.RuleKit.forbid;
import static com.cloudforgeci.api.core.rules.RuleKit.when;
import static com.cloudforgeci.api.core.rules.RuleKit.whenBoth;

/**
 * Universal application service topology configuration.
 *
 * <p>This topology configuration supports deploying any application that implements
 * the ApplicationSpec interface. It provides the same infrastructure patterns as
 * JENKINS_SERVICE but without hardcoding Jenkins-specific configuration.</p>
 *
 * @since 3.0.0
 */
public final class ApplicationServiceTopologyConfiguration implements TopologyConfiguration {

  private static final Logger LOG = Logger.getLogger(ApplicationServiceTopologyConfiguration.class.getName());

  @Override public TopologyType kind() { return TopologyType.APPLICATION_SERVICE; }
  @Override public String id() { return "topology:APPLICATION_SERVICE"; }

  @Override
  public List<Rule> rules(SystemContext c) {
    var r = new ArrayList<Rule>();

    // This topology supports both Fargate and EC2 runtimes.
    r.add(ctx -> (ctx.runtime != RuntimeType.FARGATE && ctx.runtime != RuntimeType.EC2)
            ? List.of("APPLICATION_SERVICE requires runtime = FARGATE or runtime = EC2") : List.of());

    // OIDC requires TLS at ALB. (Runtime profile handles cert wiring; we enforce semantics here.)
    r.add(ctx -> {
      String mode = ctx.authMode.get().orElse(null);
      Boolean sslEnabled = ctx.sslEnabled.get().orElse(false);
      if (AuthMode.ALB_OIDC == AuthMode.fromString(mode) && !sslEnabled) {
        return List.of("authMode = alb-oidc requires enableSsl = true");
      }
      return List.of();
    });

    // If enableSsl = true and caller expects DNS, they should also provide fqdn (either explicit or subdomain+domain).
    r.add(ctx -> {
      Boolean sslEnabled = ctx.sslEnabled.get().orElse(false);
      if (!sslEnabled) return List.of();
      String fqdn = ctx.fqdn.get().orElse(null);
      boolean hasFqdn = fqdn != null && !fqdn.isBlank();
      String subdomain = ctx.subdomain.get().orElse(null);
      String domain = ctx.domain.get().orElse(null);
      boolean canCompute = subdomain != null && domain != null;
      return (hasFqdn || canCompute) ? List.of() : List.of("enableSsl = true requires fqdn OR (subdomain + domain)");
    });
    // AutoScalingGroup is forbidden for Fargate runtime, but allowed for EC2 runtime
    boolean isFargate = c.runtime.equals(RuntimeType.FARGATE);
    r.add(when(isFargate , forbid("AutoScalingGroup", x -> x.asg)));

    return r;
  }

  @Override
  public void wire(SystemContext c) {

    // Auto-scaling configuration for both Fargate and EC2 services (only when maxInstanceCapacity > 1)
    Integer maxCap = c.maxInstanceCapacity.get().orElse(null);
    Integer minCap = c.minInstanceCapacity.get().orElse(null);
    boolean scale = maxCap != null && minCap != null && minCap > 0 && maxCap > 1;
    LOG.fine("=== ApplicationServiceTopologyConfiguration.wire() called ===");
    LOG.fine("minCap=" + minCap + ", maxCap=" + maxCap + ", scale=" + scale);
    LOG.fine("fargateService present: " + c.fargateService.get().isPresent());
    LOG.fine("alb present: " + c.alb.get().isPresent());
    if (scale) {
      // Fargate autoscaling - use service.autoScaleTaskCount() directly
      // Check if callback has already been registered to prevent multiple registrations
      if (!c.fargateAutoscalingCallbackRegistered.get().isPresent()) {
        LOG.fine("Registering Fargate auto-scaling callback with whenBoth");
        whenBoth(c.fargateService, c.alb, (service, alb) -> {
          // Check if Fargate autoscaling has already been configured (inside callback to prevent multiple executions)
          if (c.fargateAutoscalingConfigured.get().isPresent()) {
            LOG.info("Fargate auto-scaling already configured, skipping");
            return;
          }

          Integer min = c.minInstanceCapacity.get().orElse(1);
          Integer max = c.maxInstanceCapacity.get().orElse(1);
          Integer cpuTarget = c.cpuTargetUtilization.get().orElse(60);
          LOG.info("Configuring Fargate auto-scaling: min=" + min + ", max=" + max + ", cpuTarget=" + cpuTarget);
          ScalableTaskCount scalable = service.autoScaleTaskCount(EnableScalingProps.builder().minCapacity(min).maxCapacity(max).build());
          scalable.scaleOnCpuUtilization("CpuScaleSvc", CpuUtilizationScalingProps.builder().targetUtilizationPercent(cpuTarget)
                  .scaleInCooldown(Duration.minutes(2)).scaleOutCooldown(Duration.minutes(2)).build());
          c.fargateAutoscalingConfigured.set(true);
          LOG.info("Fargate auto-scaling configured successfully");
        });
        c.fargateAutoscalingCallbackRegistered.set(true);
      } else {
        LOG.info("Fargate auto-scaling callback already registered");
      }

      // EC2 autoscaling - add AutoScalingGroup to target group
      // Check if callback has already been registered to prevent multiple registrations
      if (!c.ec2AutoscalingCallbackRegistered.get().isPresent()) {
        whenBoth(c.asg, c.albTargetGroup, (asg, tg) -> {
          // Check if AutoScalingGroup has already been added to target group (inside callback to prevent multiple executions)
          if (c.asgAddedToTargetGroup.get().isPresent()) {
            return;
          }

          tg.addTarget(asg);
          c.asgAddedToTargetGroup.set(true);
        });
        c.ec2AutoscalingCallbackRegistered.set(true);
      } else {
      }
    }

    // DNS A/AAAA records for ALB (for both SSL and non-SSL deployments)
    // Check if DNS records callback has already been registered to prevent multiple registrations
    if (c.dnsRecordsCallbackRegistered.get().isPresent()) {
      return;
    }

    whenBoth(c.zone, c.alb, (zone, alb) -> {
      // Check if DNS records have already been created (inside callback to prevent multiple executions)
      if (c.dnsRecordsCreated.get().isPresent()) {
        return;
      }

      // Use subdomain for DNS record name, or use the domain directly if no subdomain
      String record = c.subdomain.get().orElse(null);
      if (record == null || record.isBlank()) {
        // When no subdomain is specified, use the domain name itself
        record = c.domain.get().orElse(null);
        if (record == null || record.isBlank()) {
          return; // No domain or subdomain specified, cannot create DNS records
        }
      }

      var target = RecordTarget.fromAlias(new LoadBalancerTarget(alb));
      // Include stack name in construct ID to ensure uniqueness across different deployments
      String constructIdPrefix = "ServiceAlbAlias_" + c.stackName + "_" + c.topology + "_" + c.runtime;
      new ARecord(c, constructIdPrefix + "A", ARecordProps.builder()
              .zone(zone).recordName(record).target(target).build());
      new AaaaRecord(c, constructIdPrefix + "AAAA", AaaaRecordProps.builder()
              .zone(zone).recordName(record).target(target).build());

      // Set the DNS records created flag to prevent duplicate execution
      c.dnsRecordsCreated.set(true);
    });

    // Mark that the DNS records callback has been registered
    c.dnsRecordsCallbackRegistered.set(true);
  }
}