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

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

The newest version!
package io.continual.flowcontrol.impl.controller.k8s;

import java.io.FileReader;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.introspector.PropertySubstitute;

import io.continual.builder.Builder;
import io.continual.builder.Builder.BuildFailure;
import io.continual.builder.sources.BuilderJsonDataSource;
import io.continual.flowcontrol.impl.controller.k8s.K8sElement.ElementDeployException;
import io.continual.flowcontrol.impl.controller.k8s.K8sElement.K8sDeployContext;
import io.continual.flowcontrol.impl.controller.k8s.elements.SecretDeployer;
import io.continual.flowcontrol.impl.controller.k8s.impl.NoMapImageMapper;
import io.continual.flowcontrol.impl.deployer.BaseDeployer;
import io.continual.flowcontrol.model.FlowControlCallContext;
import io.continual.flowcontrol.model.FlowControlDeployment;
import io.continual.flowcontrol.model.FlowControlDeploymentSpec;
import io.continual.flowcontrol.model.FlowControlJob.FlowControlRuntimeSpec;
import io.continual.flowcontrol.services.encryption.Encryptor;
import io.continual.iam.identity.Identity;
import io.continual.services.ServiceContainer;
import io.continual.util.data.StringUtils;
import io.continual.util.data.json.JsonVisitor;
import io.continual.util.data.json.JsonVisitor.ArrayVisitor;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.util.ClientBuilder;
import io.kubernetes.client.util.KubeConfig;

public class K8sController extends BaseDeployer
{
	static final String kSetting_ConfigMountLoc = "configMountLoc";
	static final String kDefault_ConfigMountLoc = "/var/flowcontrol/config";

	static final String kSetting_PersistMountLoc = "persistMountLoc";
	static final String kDefault_PersistMountLoc = "/var/flowcontrol/persistence";

	static final String kSetting_LogsMountLoc = "logsMountLoc";
	static final String kDefault_LogsMountLoc = "/var/flowcontrol/logs";

	static final String kSetting_InitYamlResource = "deploymentYaml";
	static final String kDefault_InitYamlResource = "initDeployment.yaml";

	static final String kSetting_ImagePullSecrets = "imagePullSecrets";

	static final String kSetting_InstallationName = "installationName";
	static final String kSetting_InternalConfigUrl = "internalConfigUrl";

	static final String kSetting_DumpInitYaml = "dumpInitYaml";

	static final String kSetting_Elements = "elements";
	
	static final String kSetting_EncryptorSvc = "encryptor";
	static final String kDefault_EncryptorSvc = "encryptor";

	static final String kSetting_ImageMapper = "imageMapper";

	static final String kSetting_UseKubeConfig = "useKubeConfig";
	static final boolean kDefault_UseKubeConfig = true;

	static final String kSetting_KubeConfigFile = "kubeConfig";
	static final String kDefault_KubeConfigFile = System.getenv("HOME") + "/.kube/config";

	static final String kSetting_k8sContext = "kubeConfigContext";
	static final String kSetting_K8sNamespace = "namespace";

	public K8sController ( ServiceContainer sc, JSONObject rawConfig ) throws BuildFailure
	{
		super ( sc, rawConfig );

		// evaluate the config in bulk...
		final JSONObject config = sc.getExprEval ().evaluateJsonObject ( rawConfig );

		// get k8s client config setup
		fK8sNamespace = config.getString ( kSetting_K8sNamespace );
		setupK8sConfig ( config, fK8sNamespace );

		// get the image mapper
		final JSONObject mapperSpec = config.optJSONObject ( kSetting_ImageMapper );
		if ( mapperSpec != null )
		{
			log.info ( "Building image mapper from {} setting.", kSetting_ImageMapper );
			fImageMapper = Builder.fromJson ( ContainerImageMapper.class, mapperSpec, sc );
		}
		else
		{
			log.info ( "Using default (name:version) image mapper" );
			fImageMapper = new NoMapImageMapper ();
		}

		// an encryption service
		fEncryptor = sc.getReqd ( config.optString ( kSetting_EncryptorSvc, kDefault_EncryptorSvc ), Encryptor.class );

		// kubernetes API settings
		fImgPullSecrets = JsonVisitor.arrayToList ( config.optJSONArray ( kSetting_ImagePullSecrets ) );

		//
		//	FIXME: the remaining settings feel like they should be in a list of arbitrary items that
		//	meet a container image spec for the system.  For example, why can't this system just dictate
		//	"here's where to put your logs?" why not always put them into /opt/logs, for example, and 
		//	mount a volume there (or don't)....  Or why does this system need to dictate to the processing
		//	engine container where to find its config? Why can't that image just deal with the URL 
		//	provided in any way it prefers?
		//

		// on-pod mount points
		fConfigMountLoc = config.optString ( kSetting_ConfigMountLoc, kDefault_ConfigMountLoc );
		fPersistMountLoc = config.optString ( kSetting_PersistMountLoc, kDefault_PersistMountLoc );
		fLogsMountLoc = config.optString ( kSetting_LogsMountLoc, kDefault_LogsMountLoc );

		fInstallationName = config.optString ( kSetting_InstallationName, "" );
		fInternalConfigBaseUrl = config.optString ( kSetting_InternalConfigUrl, "localhost:8080" );

		fElements = new LinkedList<> ();
		JsonVisitor.forEachElement ( config.optJSONArray ( kSetting_Elements ), new ArrayVisitor ()
		{
			@Override
			public boolean visit ( JSONObject element ) throws JSONException, BuildFailure
			{
				final K8sElement elementBuilder = Builder.withBaseClass ( K8sElement.class )
					.withClassNameInData ()
					.searchingPath ( SecretDeployer.class.getPackageName () )
					.usingData ( new BuilderJsonDataSource ( element ) )
					.build ()
				;
				fElements.add ( elementBuilder );
				return true;
			}
		} );
	}

	
	@Override
	protected FlowControlDeployment internalDeploy ( FlowControlCallContext ctx, FlowControlDeploymentSpec ds, String configKey ) throws ServiceException, RequestException
	{
		try
		{
			// setup job for transfer via config transfer service
			final String jobId = ds.getJob ().getId ();
			final String k8sDeployId = makeK8sName ( jobId );
			final String tag = k8sDeployId;

			// get the runtime spec 
			final FlowControlRuntimeSpec runtimeSpec = ds.getJob ().getRuntimeSpec ();
			if ( runtimeSpec == null ) throw new RequestException ( "There's no runtime spec on this job." );
			final String runtimeImage = fImageMapper.getImageName ( runtimeSpec );

			// build the container environment starting with what's in the spec from the user
			final HashMap env = new HashMap<> ();

			// user settings
			env.putAll ( ds.getEnv () );

			// forced environment from flow control (after user settings so they're not changed)
			env.put ( "FC_DEPLOYMENT_NAME", k8sDeployId );
			env.put ( "FC_JOB_TAG", "job-" + k8sDeployId );
			env.put ( "FC_JOB_ID", jobId );
			env.put ( "FC_CONFIG_URL", configKeyToUrl ( configKey ) );
			env.put ( "FC_CONFIG_MOUNT", fConfigMountLoc );
			env.put ( "FC_CONFIG_FILE", fConfigMountLoc + "/jobConfig.json" );
			env.put ( "FC_PERSISTENCE_MOUNT", fPersistMountLoc );
			env.put ( "FC_LOGS_MOUNT", fLogsMountLoc );
			env.put ( "FC_RUNTIME_IMAGE", runtimeImage );

			// FIXME: temporarily while balancing container setup reqs for flowcontrol vs. general use container images
			env.put ( "EP_CMDLINE_ARGS", fConfigMountLoc + "/jobConfig.json"  );
			
			// builder workspace
			final JSONObject workspace = new JSONObject ();

			final K8sDeployContext deployContext = new K8sDeployContext ()
			{
				@Override
				public String getInstallationName () { return fInstallationName; }
				@Override
				public String getNamespace () { return fK8sNamespace; }
				@Override
				public String getDeployId () { return k8sDeployId; }
				@Override
				public FlowControlDeploymentSpec getDeploymentSpec () { return ds; }
				@Override
				public String getRuntimeImage () { return runtimeImage; }
				@Override
				public Encryptor getEncryptor () { return fEncryptor; }
				@Override
				public JSONObject getWorkspace () { return workspace; }
				@Override
				public Map getEnvironment () { return env; }
				@Override
				public List getImagePullSecrets () { return fImgPullSecrets; }
			};
			
			// build each element
			for ( K8sElement element : fElements )
			{
				element.deploy ( deployContext );
			}

			return new IntDeployment ( tag, ds, ctx.getUser (), configKey );
		}
		catch ( ElementDeployException x )
		{
			throw new ServiceException ( x );
		}
	}

	@Override
	protected void internalUndeploy ( FlowControlCallContext ctx, String deploymentId, FlowControlDeployment deployment ) throws ServiceException
	{
		// build elements
		try
		{
			final LinkedList reversed = new LinkedList<> ( fElements );
			Collections.reverse ( reversed );
			for ( K8sElement element : reversed )
			{
				element.undeploy ( fK8sNamespace, deploymentId );
			}
		}
		catch ( ElementDeployException x )
		{
			throw new ServiceException ( x );
		}
	}
/*
	@Override
	public FlowControlDeployment getDeployment ( FlowControlCallContext ctx, String deploymentId ) throws ServiceException
	{
		try
		{
			final K8sDeployWrapper dw = getDeployment ( deploymentId );
			if ( dw == null ) return null;
			
			return new IntDeployment ( dw.getMetadata ().getName (), getJobIdFrom ( dw, "(unknown)" ) );
		}
		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 ( K8sDeployWrapper dw : getK8sDeployments () )
			{
				result.add ( new IntDeployment ( dw.getMetadata ().getName (), getJobIdFrom ( dw, "(unknown)" ) ) );
			}
		}
		catch ( KubernetesClientException x )
		{
			mapExceptionSvcOnly ( x );
		}
		return result;
	}

	@Override
	public List getDeploymentsForJob ( FlowControlCallContext ctx, String jobId ) throws ServiceException
	{
		final LinkedList result = new LinkedList<> ();
		try
		{
			for ( K8sDeployWrapper dw : getK8sDeployments () )
			{
				final String thisJobId = getJobIdFrom ( dw, null );
				if ( jobId.equals ( thisJobId ) )
				{
					result.add ( new IntDeployment ( dw.getMetadata ().getName (), thisJobId ) );
				}
			}
		}
		catch ( KubernetesClientException x )
		{
			mapExceptionSvcOnly ( x );
		}
		return result;
	}
*/
	private final Encryptor fEncryptor;

	private final String fK8sNamespace;
	private final String fConfigMountLoc;
	private final String fPersistMountLoc;
	private final String fLogsMountLoc;
	private final ContainerImageMapper fImageMapper;
	private final String fInstallationName;
	private final List fImgPullSecrets;
	private final LinkedList fElements;
	private final String fInternalConfigBaseUrl;

	private static final Logger log = LoggerFactory.getLogger ( K8sController.class );

	private String configKeyToUrl ( String configKey )
	{
		return StringUtils.appendIfMissing ( fInternalConfigBaseUrl, "/" )  + configKey;
	}

	private static String makeK8sName ( String from )
	{
		return from.toLowerCase ();
	}

/*
	private class K8sDeployWrapper
	{
		public K8sDeployWrapper ( Deployment d ) { fDeployment = d; fStatefulSet = null; }
		public K8sDeployWrapper ( StatefulSet ss ) { fDeployment = null; fStatefulSet = ss; }

		public io.fabric8.kubernetes.api.model.ObjectMeta getMetadata ()
		{
			if ( fDeployment != null ) return fDeployment.getMetadata (); 
			if ( fStatefulSet != null ) return fStatefulSet.getMetadata ();
			return null;
		}

		public void delete ()
		{
			if ( fDeployment != null ) fApiClient.resource ( fDeployment ).delete (); 
			if ( fStatefulSet != null ) fApiClient.resource ( fStatefulSet ).delete ();
		}

		public FlowControlRuntimeState.DeploymentStatus getStatus ()
		{
			if ( fDeployment != null )
			{
				boolean progressing = false;
				boolean available = false;
				
				final DeploymentStatus ds = fDeployment.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 FlowControlRuntimeState.DeploymentStatus.RUNNING;
				}
				else if ( progressing && !available )
				{
					return FlowControlRuntimeState.DeploymentStatus.PENDING;
				}
			}
			else if ( fStatefulSet != null )
			{
				final int replReqd = safeInt ( fStatefulSet.getSpec ().getReplicas () );
				final StatefulSetStatus sss = fStatefulSet.getStatus ();
				final int ready = safeInt ( sss.getReadyReplicas () );
				final int repls = safeInt ( sss.getReplicas () );

				log.info ( "Sts {}: {} reqd, {} created, {} ready", fStatefulSet.getMetadata ().getName (), replReqd, repls, ready );

				if ( ready < replReqd )
				{
					return FlowControlRuntimeState.DeploymentStatus.PENDING;
				}
				else if ( ready == replReqd )
				{
					return FlowControlRuntimeState.DeploymentStatus.RUNNING;
				}
			}
			return FlowControlRuntimeState.DeploymentStatus.UNKNOWN;
		}

		public int getReplicaCount ()
		{
			if ( fDeployment != null )
			{
				final DeploymentStatus ds = fDeployment.getStatus ();
				return ds.getReplicas ();
			}
			return -1;
		}
		
		private final Deployment fDeployment;
		private final StatefulSet fStatefulSet;
	}
*/
	private class IntDeployment implements FlowControlDeployment
	{
		public IntDeployment ( String tag, FlowControlDeploymentSpec ds, Identity deployer, String configKey )
		{
			fTag = tag;
			fDeployer = deployer;
			fDeploymentSpec = ds;
			fConfigKey = configKey;
		}

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

		@Override
		public FlowControlDeploymentSpec getDeploymentSpec () { return fDeploymentSpec; }

		@Override
		public Identity getDeployer () { return fDeployer; }

		@Override
		public String getConfigToken () { return fConfigKey; }

		private final String fTag;
		private final Identity fDeployer;
		private final FlowControlDeploymentSpec fDeploymentSpec;
		private final String fConfigKey;
	}
/*
	private List getLogFor ( PodResource pod, String sinceRfc3339Time )
	{
		final LinkedList result = new LinkedList<> ();

		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 );
		}

		return result;
	}
	
	private K8sDeployWrapper getDeployment ( String tag )
	{
		// First look for a stateful set. Issuing a call for a named item as a deployment and then
		// as a stateful set (since we don't know which was used in the init yaml) seemed to cause trouble
		// either for the fabric8 client or for the service, with the 2nd call throwing "not found" so instead
		// we're just grabbing the list and searching locally, and also running the stateful set search first
		// since it's our normal.

		try
		{
			// get the stateful set list
			final StatefulSetList ssl = fApiClient.apps().statefulSets ().inNamespace ( fK8sNamespace ).list ();
			for ( StatefulSet ss : ssl.getItems () )
			{
				if ( ss.getMetadata ().getName ().equals ( tag ) )
				{
					return new K8sDeployWrapper ( ss );
				}
			}
		}
		catch ( KubernetesClientException | IllegalStateException x )
		{
			// spec says object should be null if it doesn't exist, but testing implies this exception is thrown instead
			log.warn ( x.getMessage () );
		}

		try
		{
			// now try deployment list
			final DeploymentList dl = fApiClient.apps().deployments ().inNamespace ( fK8sNamespace ).list ();
			for ( Deployment d : dl.getItems () )
			{
				if ( d.getMetadata ().getName ().equals ( tag ) )
				{
					return new K8sDeployWrapper ( d );
				}
			}
		}
		catch ( KubernetesClientException | IllegalStateException x )
		{
			// spec says object should be null if it doesn't exist, but testing implies this exception is thrown instead
			log.warn ( x.getMessage () );
		}

		return null;
	}

	private List getK8sDeployments ( )
	{
		final LinkedList result = new LinkedList<> ();
		for ( Deployment d : fApiClient.apps().deployments().inNamespace ( fK8sNamespace ).list ().getItems () )
		{
			result.add ( new K8sDeployWrapper ( d ) );
		}
		for ( StatefulSet d : fApiClient.apps().statefulSets ().inNamespace ( fK8sNamespace ).list ().getItems () )
		{
			result.add ( new K8sDeployWrapper ( d ) );
		}
		return result;
	}

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

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

	private static int safeInt ( Integer i )
	{
		return i == null ? 0 : i;
	}

	private static String getJobIdFrom ( K8sDeployWrapper dw, String defval )
	{
		// make no assumptions about the existence of structures here!
		if ( dw != null )
		{
			final ObjectMeta om = dw.getMetadata ();
			if ( om != null )
			{
				final Map labels = om.getLabels ();
				if ( labels != null )
				{
					final String jobId = labels.get ( "flowcontroljob" );
					if ( jobId != null )
					{
						return jobId;
					}
				}
			}
		}
		return defval;
	}
*/
	private void setupK8sConfig ( JSONObject config, String namespace ) throws BuildFailure
	{
		// We can run in-cluster or via kube config file. Adapted from https://github.com/kubernetes-client/java/wiki/3.-Code-Examples, Oct 2024 
		try
		{
			final ApiClient client;

			final boolean useKubeConfig = config.optBoolean ( kSetting_UseKubeConfig, kDefault_UseKubeConfig );
			final String kubeConfigFile = config.optString ( kSetting_KubeConfigFile, kDefault_KubeConfigFile );

			if ( useKubeConfig || ( config.has ( kSetting_KubeConfigFile ) && StringUtils.isNotEmpty ( kubeConfigFile ) ) )
			{
				log.info ( "Building k8s API config from kube config [" + kubeConfigFile + "]" );

				// user has asked for kube config read either via use-kube-config boolean or by setting a
				// non-empty filename
				final KubeConfig kc = KubeConfig.loadKubeConfig ( new FileReader ( kubeConfigFile ) );
				
				// get the k8s context name
				final String contextName = config.optString ( kSetting_k8sContext, null );
				if ( StringUtils.isNotEmpty ( contextName ) )
				{
					log.info ( "Using kubectl context [{}]", contextName );
					kc.setContext ( contextName );
				}
				else
				{
					log.warn ( "🤔 Using kubectl's current context. (It's a good idea to explicitly configure '{}'.)", kSetting_k8sContext );
				}

				// build
				client = ClientBuilder
					.kubeconfig ( kc )
					.build ()
				;
			}
			else
			{
				log.info ( "Building k8s API config from in-cluster service account data" );

				// use service account setup
				client = ClientBuilder.cluster ().build ();

			    // if you prefer not to refresh service account token, please use:
			    // ApiClient client = ClientBuilder.oldCluster().build();
			}

		    // set the global default api-client to the in-cluster one from above
			Configuration.setDefaultApiClient ( client );

			// test connectivity by checking for the assigned namespace
			final CoreV1Api api = new CoreV1Api ();
			if ( 0 == api
				.listNamespace ()
				.labelSelector ( "kubernetes.io/metadata.name=" + namespace )
				.execute ()
				.getItems ()
				.size () 
			)
			{
				throw new BuildFailure ( "Namespace [" + namespace + "] is not available." );
			}
			log.info ( "Connected to Kubernetes and found namespace [" + namespace + "]." );
		}
		catch ( IOException x )
		{
			throw new BuildFailure ( x );
		}
		catch ( ApiException x )
		{
			throw new BuildFailure ( x );
		}
	}

	// see https://github.com/kubernetes-client/java/issues/2741 (this doesn't seem to be helping any)
	static
	{
		java.util.logging.Logger snakeLog = java.util.logging.Logger.getLogger ( PropertySubstitute.class.getPackage().getName() );
		snakeLog.setLevel ( java.util.logging.Level.SEVERE );
		snakeLog = java.util.logging.Logger.getLogger ( "org.yaml.snakeyaml.introspector" );
		snakeLog.setLevel ( java.util.logging.Level.SEVERE );
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy