All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.continual.flowcontrol.impl.controller.k8s.K8sController Maven / Gradle / Ivy

There is a newer version: 0.3.23
Show newest version
package io.continual.flowcontrol.impl.controller.k8s;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.json.JSONObject;

import io.continual.builder.Builder;
import io.continual.builder.Builder.BuildFailure;
import io.continual.flowcontrol.FlowControlCallContext;
import io.continual.flowcontrol.controlapi.ConfigTransferService;
import io.continual.flowcontrol.controlapi.FlowControlDeployment;
import io.continual.flowcontrol.controlapi.FlowControlDeploymentService;
import io.continual.flowcontrol.controlapi.FlowControlRuntimeSpec;
import io.continual.flowcontrol.jobapi.FlowControlJob;
import io.continual.services.ServiceContainer;
import io.continual.services.SimpleService;
import io.continual.util.data.StreamTools;
import io.continual.util.data.TypeConvertor;
import io.continual.util.data.UniqueStringGenerator;
import io.continual.util.standards.HttpStatusCodes;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarSource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodList;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.SecretKeySelector;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentCondition;
import io.fabric8.kubernetes.api.model.apps.DeploymentStatus;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.PodResource;

public class K8sController extends SimpleService implements FlowControlDeploymentService
{
	static final String kSetting_k8sContext = "context";
	static final String kSetting_Namespace = "namespace";

	static final String kSetting_StorageClass = "storageClass";
	static final String kDefault_StorageClass = "";

	static final String kSetting_ConfigMountLoc = "configMountLoc";
	static final String kDefault_ConfigMountLoc = "/var/flowcontrol";

	static final String kSetting_ConfigTransfer = "configTransfer";

	public K8sController ( ServiceContainer sc, JSONObject rawConfig ) throws BuildFailure
	{
		final JSONObject config = sc.getExprEval ().evaluateJsonObject ( rawConfig );

		fConfigTransfer = sc.get ( config.optString ( kSetting_ConfigTransfer, "configTransfer" ), ConfigTransferService.class );
		if ( fConfigTransfer == null ) throw new BuildFailure ( "No configTransfer service" );

		final String contextName = config.optString ( kSetting_k8sContext, null );
		if ( contextName != null )
		{
			final Config cfgWithContext = Config.autoConfigure ( contextName );
			fApiClient = new DefaultKubernetesClient ( cfgWithContext );
		}
		else
		{
			fApiClient = new DefaultKubernetesClient ();
		}
		fNamespace = config.getString ( kSetting_Namespace );
		fStorageClass = config.optString ( kSetting_StorageClass, kDefault_StorageClass );
		fConfigMountLoc = config.optString ( kSetting_ConfigMountLoc, kDefault_ConfigMountLoc );

		final JSONObject mapperSpec = config.optJSONObject ( "imageMapper" );
		if ( mapperSpec != null )
		{
			fImageMapper = Builder.fromJson ( ContainerImageMapper.class, mapperSpec, sc );
		}
		else
		{
			fImageMapper = new SimpleImageMapper ();
		}
	}

	@Override
	protected void onStopRequested ()
	{
		super.onStopRequested ();

		fApiClient.close ();
	}

	@Override
	public DeploymentSpecBuilder deploymentBuilder ()
	{
		return new LocalDeploymentSpecBuilder ();
	}

	@Override
	public FlowControlDeployment deploy ( FlowControlCallContext ctx, DeploymentSpec ds ) throws ServiceException, RequestException
	{
		try
		{
			final String jobId = ds.getJob ().getId ();
			final String tag = UniqueStringGenerator.createKeyUsingAlphabet ( jobId, "abcdefhigjklmnopqrstuvwxyz" );
			final Map configFetchEnv = fConfigTransfer.deployConfiguration ( ds.getJob () );

			final String targetConfigFile = fConfigMountLoc + "/jobConfig.json";
			
			// warning: don't use a key that's a substring of another key, because we don't pay attn to order here
			final HashMap replacements = new HashMap<>();
			replacements.put ( "FC_DEPLOYMENT_NAME", tag );
			replacements.put ( "FC_JOB_TAG", "job-" + tag );
			replacements.put ( "FC_JOB_ID", jobId );
			replacements.put ( "FC_STORAGE_CLASS", fStorageClass );
			replacements.put ( "FC_INSTANCE_COUNT", "" + ds.getInstanceCount () );
			replacements.put ( "FC_RUNTIME_IMAGE", fImageMapper.getImageName ( ds.getJob ().getRuntimeSpec () ) );
			replacements.put ( "FC_CONFIG_MOUNT", fConfigMountLoc );
			replacements.put ( "FC_CONFIG_FILE", targetConfigFile );
			replacements.put ( "FC_INITER_IMAGE", "busybox:1.28" );

			// place any secrets from this job
			final String secretsName = tagToSecret ( tag );

			// start a secret for this job's secret data
			final Map secrets = ds.getJob ().getSecrets ();
			SecretBuilder sb = new SecretBuilder ()
				.withType ( "Opaque" )
				.withNewMetadata ()
					.withName ( secretsName )
				.endMetadata ()
			;
			boolean anyInternalSecrets = false;
			for ( Map.Entry secret : secrets.entrySet () )
			{
				final String val = secret.getValue ();
				final boolean isInternal = val != null;

				if ( isInternal )
				{
					anyInternalSecrets = true;
					sb = sb.addToData ( secret.getKey (), TypeConvertor.base64Encode ( secret.getValue () ) );
				}
			}
			if ( anyInternalSecrets )
			{
				fApiClient.secrets ().inNamespace ( fNamespace ).createOrReplace ( sb.build () );
			}

			// get deployment installed
			try ( final InputStream deployTemplate = getClass ().getResourceAsStream ( "initDeployment.yaml" ) )
			{
				if ( deployTemplate == null ) throw new ServiceException ( "Couldn't load resource yaml" );
				final List items = fApiClient.load ( replaceAllTokens ( deployTemplate, replacements ) ).get ();

				// push environment
				final HashMap env = new HashMap ();
				env.putAll ( ds.getEnv () );
				env.putAll ( configFetchEnv );
				env.put ( "FC_CONFIG_DIR", fConfigMountLoc );
				env.put ( "CONFIG_FILE", targetConfigFile );

				for ( HasMetadata md : items )
				{
					if ( md.getKind ().equals ( "Deployment" ) )
					{
						final Deployment d = (Deployment) md;
						final PodSpec ps = d.getSpec ().getTemplate ().getSpec ();

						for ( Container c : ps.getContainers () )
						{
							pushEnvMapToContainer ( env, c );
							addSecretsToContainer ( secretsName, secrets, c );
						}
						for ( Container c : ps.getInitContainers () )
						{
							pushEnvMapToContainer ( env, c );
							addSecretsToContainer ( secretsName, secrets, c );
						}
					}
				}
				
				fApiClient
					.resourceList ( items )
					.inNamespace ( fNamespace )
					.createOrReplace ()
				;
			}
			catch ( IOException e )
			{
				throw new ServiceException ( e );
			}
			catch ( KubernetesClientException x )
			{
				mapException ( x );
			}
	
			return new IntDeployment ( tag, jobId );
		}
		catch ( ConfigTransferService.ServiceException x )
		{
			throw new ServiceException ( x );
		}
		catch ( io.continual.flowcontrol.jobapi.FlowControlJobDb.ServiceException x )
		{
			throw new ServiceException ( x );
		}
	}

	@Override
	public void undeploy ( FlowControlCallContext ctx, String deploymentId ) throws ServiceException
	{
		// FIXME: does user own deployment?

		try
		{
			final Deployment d = fApiClient.apps().deployments ().inNamespace ( fNamespace ).withName ( deploymentId ).get ();
			if ( d != null )
			{
				fApiClient.resource ( d ).delete ();
			}
		}
		catch ( KubernetesClientException | IllegalStateException x )
		{
			// spec says object should be null if it doesn't exist, but testing implies this exception is thrown instead
		}
	
		try
		{
			final Secret secret = fApiClient.secrets ().inNamespace ( fNamespace ).withName ( tagToSecret ( deploymentId ) ).get ();
			if ( secret != null )
			{
				fApiClient.resource ( secret ).delete ();
			}
		}
		catch ( KubernetesClientException | IllegalStateException x )
		{
			// spec says object should be null if it doesn't exist, but testing implies this exception is thrown instead
		}
	}

	@Override
	public FlowControlDeployment getDeployment ( FlowControlCallContext ctx, String deploymentId ) throws ServiceException
	{
		try
		{
			final Deployment d = fApiClient.apps().deployments().inNamespace ( fNamespace ).withName ( deploymentId ).get ();
			if ( d == null ) return null;
			
			final String jobId = d.getMetadata ().getLabels ().get ( "flowcontroljob" );
			return new IntDeployment ( d.getMetadata ().getName (), jobId == null ? "(unknown)" : jobId );
		}
		catch ( KubernetesClientException x )
		{
			final Throwable cause = x.getCause ();
			if ( cause instanceof java.net.ProtocolException )
			{
				// this is a "not found" symptom
				return null;
			}
			
			mapExceptionSvcOnly ( x );
			return null;	// unreachable
		}
		catch ( IllegalStateException x )
		{
			return null;
		}
	}

	@Override
	public List getDeployments ( FlowControlCallContext ctx ) throws ServiceException
	{
		final LinkedList result = new LinkedList<> ();
		try
		{
			for ( Deployment d : fApiClient.apps().deployments().inNamespace ( fNamespace ).list ().getItems ()  )
			{
				final String jobId = d.getMetadata ().getLabels ().get ( "flowcontroljob" );
				result.add ( new IntDeployment ( d.getMetadata ().getName (), jobId == null ? "(unknown)" : jobId ) );
			}
		}
		catch ( KubernetesClientException x )
		{
			mapExceptionSvcOnly ( x );
		}
		return result;
	}

	@Override
	public List getDeploymentsForJob ( FlowControlCallContext ctx, String jobId ) throws ServiceException
	{
		final LinkedList result = new LinkedList<> ();
		try
		{
			for ( Deployment d : fApiClient.apps().deployments().inNamespace ( fNamespace ).list ().getItems ()  )
			{
				final String thisJobId = d.getMetadata ().getLabels ().get ( "flowcontroljob" );
				if ( jobId.equals ( thisJobId ) )
				{
					result.add ( new IntDeployment ( d.getMetadata ().getName (), thisJobId ) );
				}
			}
		}
		catch ( KubernetesClientException x )
		{
			mapExceptionSvcOnly ( x );
		}
		return result;
	}

	private final ConfigTransferService fConfigTransfer;
	private final KubernetesClient fApiClient;
	private final String fNamespace;
	private final String fStorageClass;
	private final String fConfigMountLoc;
	private final ContainerImageMapper fImageMapper;

	private static class SimpleImageMapper implements ContainerImageMapper
	{
		@Override
		public String getImageName ( FlowControlRuntimeSpec rs )
		{
			return rs.getName () + ":" + rs.getVersion ();
		}
	}

	private static String tagToSecret ( String tag )
	{
		return "secret-" + tag;
	}

	private void addSecretsToContainer ( String secretName, Map secrets, Container c )
	{
		List list = c.getEnv ();

		for ( Map.Entry e : secrets.entrySet () )
		{
			if ( e.getValue () != null )
			{
				list.add ( new EnvVar ( e.getKey (), null, new EnvVarSource ( null, null, null, new SecretKeySelector ( e.getKey (), secretName, true ) ) ) );
			}
		}
	}

	private void pushEnvMapToContainer ( Map env, Container c )
	{
		List list = c.getEnv ();
		for ( Map.Entry e : env.entrySet () )
		{
			list.add ( new EnvVar ( e.getKey (), e.getValue (), null ) );
		}
	}

	private static void mapException ( KubernetesClientException x ) throws RequestException, ServiceException
	{
		// relay this exception...
		final int status = x.getStatus ().getCode ();
		if ( HttpStatusCodes.isClientFailure ( status ) )
		{
			switch ( status )
			{
				case HttpStatusCodes.k404_notFound:
					throw new FlowControlDeploymentService.RequestException ( "Object not found." );
				case HttpStatusCodes.k400_badRequest:
					throw new FlowControlDeploymentService.RequestException ( "Bad request." );
				default:
					throw new FlowControlDeploymentService.RequestException ( x );
			}
		}
		else
		{
			throw new FlowControlDeploymentService.ServiceException ( x );
		}
	}

	private static void mapExceptionSvcOnly ( KubernetesClientException x ) throws ServiceException
	{
		throw new FlowControlDeploymentService.ServiceException ( x );
	}

	private class IntDeployment implements FlowControlDeployment
	{
		public IntDeployment ( String tag, String jobId )
		{
			fTag = tag;
			fJobId = jobId;
		}

		@Override
		public String getId ()
		{
			return fTag;
		}

		@Override
		public String getJobId ()
		{
			return fJobId;
		}

		@Override
		public Status getStatus ()
		{
			final Deployment d = getDeployment ( fTag );
			if ( d != null )
			{
				boolean progressing = false;
				boolean available = false;
				
				final DeploymentStatus ds = d.getStatus ();
				for ( DeploymentCondition dc : ds.getConditions () )
				{
					final String type = dc.getType ();
					final String status = dc.getStatus ();

					if ( type.equalsIgnoreCase ( "progressing" ) && status.equalsIgnoreCase ( "true" ) )
					{
						progressing = true;
					}
					else if ( type.equalsIgnoreCase ( "available" ) && status.equalsIgnoreCase ( "true" ) )
					{
						available = true;
					}
				}

				if ( progressing && available )
				{
					return Status.RUNNING;
				}
				else if ( progressing && !available )
				{
					return Status.PENDING;
				}
			}

			return Status.UNKNOWN;
		}

		@Override
		public int instanceCount ()
		{
			final Deployment d = getDeployment ( fTag );
			if ( d != null )
			{
				final DeploymentStatus ds = d.getStatus ();
				return ds.getReplicas ();
			}

			return -1;
		}

		@Override
		public Set instances ()
		{
			final TreeSet result = new TreeSet<> ();
			final List pods = getPodsFor ( fTag );
			for ( Pod p : pods )
			{
				result.add ( p.getMetadata ().getName () );
			}
			return result;
		}

		@Override
		public List getLog ( String instanceId, String sinceRfc3339Time ) throws RequestException, ServiceException
		{
			final LinkedList result = new LinkedList<> ();
			try
			{
				final PodResource pod = fApiClient.pods ()
					.inNamespace ( fNamespace )
					.withName ( instanceId )
				;

				final String logText;
				if ( sinceRfc3339Time != null )
				{
					logText = pod.sinceTime ( sinceRfc3339Time ).getLog (); 
				}
				else
				{
					logText = pod.getLog ();
				}

				final String[] lines = logText.split ( "\\n" );
				for ( String line : lines )
				{
					result.add ( line );
				}
			}
			catch ( KubernetesClientException x )
			{
				mapException ( x );
			}
			return result;
		}

		private final String fTag;
		private final String fJobId;
	}

	private Deployment getDeployment ( String tag )
	{
		return fApiClient.apps().deployments ().inNamespace ( fNamespace ).withName ( tag ).get ();
	}

	private List getPodsFor ( String tag )
	{
		final PodList pl = fApiClient
			.pods ()
			.inNamespace ( fNamespace )
			.withLabel ( "app", "job-" + tag )

			.list ()
		;
		return pl.getItems ();
	}

	// this is intended for small scale stream handling only
	private static InputStream replaceAllTokens ( InputStream src, Map replacements ) throws IOException
	{
		final byte[] bytes = StreamTools.readBytes ( src );
		final String origText = new String ( bytes, StandardCharsets.UTF_8 );
		String newText = origText;
		for ( Map.Entry e : replacements.entrySet () )
		{
			newText = newText.replaceAll ( e.getKey (), e.getValue () );
		}
		return new ByteArrayInputStream ( newText.getBytes ( StandardCharsets.UTF_8 ) );
	}

	private static class LocalDeploymentSpecBuilder implements DeploymentSpecBuilder
	{
		@Override
		public DeploymentSpecBuilder forJob ( FlowControlJob job )
		{
			fJob = job;
			return this;
		}

		@Override
		public DeploymentSpecBuilder withInstances ( int count )
		{
			fInstances = count;
			return this;
		}

		@Override
		public DeploymentSpecBuilder withEnv ( String key, String val )
		{
			fEnv.put ( key, val );
			return this;
		}

		@Override
		public DeploymentSpecBuilder withEnv ( Map keyValMap )
		{
			fEnv.putAll ( keyValMap );
			return this;
		}

		@Override
		public DeploymentSpec build () throws BuildFailure
		{
			if ( fJob == null ) throw new BuildFailure ( "No job provided." );
			return new LocalDeploymentSpec ( this );
		}

		private FlowControlJob fJob;
		private int fInstances = 1;
		private HashMap fEnv = new HashMap<> ();
	}

	private static class LocalDeploymentSpec implements DeploymentSpec
	{
		private final LocalDeploymentSpecBuilder fBuilder;

		public LocalDeploymentSpec ( LocalDeploymentSpecBuilder builder )
		{
			fBuilder = builder;
		}

		@Override
		public FlowControlJob getJob () { return fBuilder.fJob; }

		@Override
		public int getInstanceCount () { return fBuilder.fInstances; }

		@Override
		public Map getEnv () { return fBuilder.fEnv; }
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy