From 594bcc84031c9306f9847b0a5a810d6aa2679c15 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Fri, 1 Mar 2019 17:54:47 -0800 Subject: [PATCH 01/22] Implementing authentication for Pulsar Functions --- bin/pulsar-admin | 1 - conf/functions_worker.yml | 6 +- .../AuthenticationDataSource.java | 9 +++ .../AuthenticationProviderToken.java | 2 +- .../broker/admin/impl/FunctionsBase.java | 2 +- .../pulsar/broker/admin/impl/SinkBase.java | 2 +- .../pulsar/broker/admin/impl/SourceBase.java | 2 +- .../instance/AuthenticationConfig.java | 1 + .../proto/src/main/proto/Function.proto | 5 ++ pulsar-functions/runtime/pom.xml | 6 ++ .../functions/runtime/KubernetesRuntime.java | 26 +++++++ .../runtime/KubernetesRuntimeFactory.java | 71 +++++++++++++++++++ .../functions/runtime/RuntimeFactory.java | 5 ++ .../functions/runtime/RuntimeUtils.java | 39 +++++----- .../worker/rest/api/ComponentImpl.java | 27 +++++-- .../worker/rest/api/FunctionsImplV2.java | 2 +- .../rest/api/v3/FunctionApiV3Resource.java | 2 +- .../worker/rest/api/v3/SinkApiV3Resource.java | 4 +- .../rest/api/v3/SourceApiV3Resource.java | 3 +- .../api/v3/FunctionApiV3ResourceTest.java | 9 ++- .../rest/api/v3/SinkApiV3ResourceTest.java | 6 +- .../rest/api/v3/SourceApiV3ResourceTest.java | 6 +- 22 files changed, 189 insertions(+), 47 deletions(-) diff --git a/bin/pulsar-admin b/bin/pulsar-admin index 97206e61f4cdc..482a98d6ce813 100755 --- a/bin/pulsar-admin +++ b/bin/pulsar-admin @@ -17,7 +17,6 @@ # specific language governing permissions and limitations # under the License. # - BINDIR=$(dirname "$0") export PULSAR_HOME=`cd $BINDIR/..;pwd` . "$PULSAR_HOME/bin/pulsar-admin-common.sh" diff --git a/conf/functions_worker.yml b/conf/functions_worker.yml index 0c1125d770352..865da5b2325d6 100644 --- a/conf/functions_worker.yml +++ b/conf/functions_worker.yml @@ -44,10 +44,12 @@ downloadDirectory: /tmp/pulsar_functions # points pulsarServiceUrl: pulsar://localhost:6650 pulsarWebServiceUrl: http://localhost:8080 + # the authentication plugin to be used by the pulsar client used in worker service -# clientAuthenticationPlugin: +clientAuthenticationPlugin: org.apache.pulsar.client.impl.auth.AuthenticationToken # the authentication parameter to be used by the pulsar client used in worker service -# clientAuthenticationParameters: +clientAuthenticationParameters: file:///etc/auth/token + # pulsar topics used for function metadata management diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java index 9fc6cbe532b3d..70cab841f6bf7 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java @@ -117,4 +117,13 @@ default boolean hasDataFromPeer() { default SocketAddress getPeerAddress() { return null; } + + + default boolean isAuthDataForwardable() { + return false; + } + + default byte[] getData() { + return new byte[0]; + } } diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java index c2bd63e5d5dc4..a4ffceb815378 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java @@ -35,7 +35,7 @@ public class AuthenticationProviderToken implements AuthenticationProvider { final static String HTTP_HEADER_NAME = "Authorization"; - final static String HTTP_HEADER_VALUE_PREFIX = "Bearer "; + public final static String HTTP_HEADER_VALUE_PREFIX = "Bearer "; // When symmetric key is configured final static String CONF_TOKEN_SECRET_KEY = "tokenSecretKey"; diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/FunctionsBase.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/FunctionsBase.java index 9b88f29a96158..c4fd33f3b8387 100644 --- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/FunctionsBase.java +++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/FunctionsBase.java @@ -82,7 +82,7 @@ public void registerFunction(final @PathParam("tenant") String tenant, final @FormDataParam("functionConfig") String functionConfigJson) { functions.registerFunction(tenant, namespace, functionName, uploadedInputStream, fileDetail, - functionPkgUrl, null, functionConfigJson, clientAppId()); + functionPkgUrl, null, functionConfigJson, clientAppId(), clientAuthData()); } @PUT diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SinkBase.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SinkBase.java index 2bd22a4d3f6e2..ffd4dd7847d3b 100644 --- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SinkBase.java +++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SinkBase.java @@ -79,7 +79,7 @@ public void registerSink(final @PathParam("tenant") String tenant, final @FormDataParam("sinkConfig") String sinkConfigJson) { sink.registerFunction(tenant, namespace, sinkName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sinkConfigJson, clientAppId()); + functionPkgUrl, null, sinkConfigJson, clientAppId(), clientAuthData()); } @PUT diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SourceBase.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SourceBase.java index 0e8348fa10738..6078ede19a340 100644 --- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SourceBase.java +++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/SourceBase.java @@ -79,7 +79,7 @@ public void registerSource(final @PathParam("tenant") String tenant, final @FormDataParam("sourceConfig") String sourceConfigJson) { source.registerFunction(tenant, namespace, sourceName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sourceConfigJson, clientAppId()); + functionPkgUrl, null, sourceConfigJson, clientAppId(), clientAuthData()); } @PUT diff --git a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java index f4c78eac66412..864abaee564cb 100644 --- a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java +++ b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java @@ -35,6 +35,7 @@ @ToString @Builder public class AuthenticationConfig { + private String functionAuthProviderPlugin; private String clientAuthenticationPlugin; private String clientAuthenticationParameters; private String tlsTrustCertsFilePath; diff --git a/pulsar-functions/proto/src/main/proto/Function.proto b/pulsar-functions/proto/src/main/proto/Function.proto index 8c93b3a89309d..2bc5ea8581ef9 100644 --- a/pulsar-functions/proto/src/main/proto/Function.proto +++ b/pulsar-functions/proto/src/main/proto/Function.proto @@ -66,6 +66,11 @@ message FunctionDetails { Resources resources = 13; string packageUrl = 14; //present only if function submitted with package-url RetryDetails retryDetails = 15; + FunctionAuthenticationSpec functionAuthSpec = 17; +} + +message FunctionAuthenticationSpec { + string id = 1; } message ConsumerSpec { diff --git a/pulsar-functions/runtime/pom.xml b/pulsar-functions/runtime/pom.xml index f3176a79649d8..ca363c8d49a2a 100644 --- a/pulsar-functions/runtime/pom.xml +++ b/pulsar-functions/runtime/pom.xml @@ -63,6 +63,12 @@ + + org.apache.pulsar + pulsar-broker-common + ${project.version} + + diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java index 3fc0c697ad5df..5fcd3a9ac13b6 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java @@ -44,12 +44,15 @@ import io.kubernetes.client.models.V1PodSpec; import io.kubernetes.client.models.V1PodTemplateSpec; import io.kubernetes.client.models.V1ResourceRequirements; +import io.kubernetes.client.models.V1SecretVolumeSource; import io.kubernetes.client.models.V1Service; import io.kubernetes.client.models.V1ServicePort; import io.kubernetes.client.models.V1ServiceSpec; import io.kubernetes.client.models.V1StatefulSet; import io.kubernetes.client.models.V1StatefulSetSpec; import io.kubernetes.client.models.V1Toleration; +import io.kubernetes.client.models.V1Volume; +import io.kubernetes.client.models.V1VolumeMount; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.pulsar.functions.instance.AuthenticationConfig; @@ -794,6 +797,7 @@ private V1StatefulSet createStatefulSet() { statefulSet.spec(statefulSetSpec); + return statefulSet; } @@ -846,6 +850,19 @@ private V1PodSpec getPodSpec(List instanceCommand, Function.Resources re containers.add(getFunctionContainer(instanceCommand, resource)); podSpec.containers(containers); + // set authentication token + Function.FunctionAuthenticationSpec authenticationSpec = instanceConfig.getFunctionDetails().getFunctionAuthSpec(); + if (authenticationSpec != null) { + podSpec.setVolumes(Collections.singletonList( + new V1Volume() + .name("function-auth") + .secret( + new V1SecretVolumeSource() + .secretName(authenticationSpec.getId()) + + .defaultMode(256)))); + } + // Configure secrets secretsProviderConfigurator.configureKubernetesRuntimeSecretsProvider(podSpec, PULSARFUNCTIONS_CONTAINER_NAME, instanceConfig.getFunctionDetails()); @@ -905,6 +922,15 @@ V1Container getFunctionContainer(List instanceCommand, Function.Resource // set container ports container.setPorts(getFunctionContainerPorts()); + // auth + if (instanceConfig.getFunctionDetails().getFunctionAuthSpec()) + container.setVolumeMounts(Collections.singletonList( + new V1VolumeMount() + .name("function-auth") + .mountPath(authPath) +// .mountPath("/etc/auth") + .readOnly(true))); + return container; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java index 6a551c2589706..939c63182617b 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java @@ -21,10 +21,13 @@ import com.google.common.annotations.VisibleForTesting; import io.kubernetes.client.ApiClient; +import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; import io.kubernetes.client.apis.AppsV1Api; import io.kubernetes.client.apis.CoreV1Api; import io.kubernetes.client.models.V1ConfigMap; +import io.kubernetes.client.models.V1ObjectMeta; +import io.kubernetes.client.models.V1Secret; import io.kubernetes.client.util.Config; import java.nio.file.Paths; import lombok.Getter; @@ -32,18 +35,24 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.common.functions.Resources; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; import org.apache.pulsar.functions.secretsproviderconfigurator.SecretsProviderConfigurator; +import javax.naming.AuthenticationException; import java.lang.reflect.Field; +import java.util.Collections; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.UUID; import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; +import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; /** * Kubernetes based function container factory implementation. @@ -309,4 +318,66 @@ void validateMinResourcesRequired(Function.FunctionDetails functionDetails) { } } } + + @Override + public Function.FunctionAuthenticationSpec cacheAuthData(AuthenticationDataSource authenticationDataSource) { + String id = null; + try { + String token = getToken(authenticationDataSource); + if (token != null) { + id = createSecret(token); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (id != null) { + return Function.FunctionAuthenticationSpec.newBuilder().setId(id).build(); + } + return null; + } + + static final String FUNCTION_AUTH_TOKEN = "token"; + private String createSecret(String token) throws ApiException { + try { + setupClient(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + String id = UUID.randomUUID().toString(); + V1Secret v1Secret = new V1Secret() + .metadata(new V1ObjectMeta().name(id)) + .data(Collections.singletonMap(FUNCTION_AUTH_TOKEN, token.getBytes())); + coreClient.createNamespacedSecret(kubernetesInfo.getJobNamespace(), v1Secret, "true"); + return id; + } + + private String getToken(AuthenticationDataSource authData) throws AuthenticationException { + if (authData.hasDataFromCommand()) { + // Authenticate Pulsar binary connection + return authData.getCommandData(); + } else if (authData.hasDataFromHttp()) { + // Authentication HTTP request. The format here should be compliant to RFC-6750 + // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx + String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); + if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { + throw new AuthenticationException("Invalid HTTP Authorization header"); + } + + // Remove prefix + String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); + return validateToken(token); + } else { + return null; + } + } + + private String validateToken(final String token) throws AuthenticationException { + if (StringUtils.isNotBlank(token)) { + return token; + } else { + throw new AuthenticationException("Blank token found"); + } + } } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java index 248afbb55d7dd..c47999335e69a 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java @@ -19,6 +19,7 @@ package org.apache.pulsar.functions.runtime; +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; @@ -43,6 +44,10 @@ Runtime createContainer( default void doAdmissionChecks(Function.FunctionDetails functionDetails) { } + default Function.FunctionAuthenticationSpec cacheAuthData(AuthenticationDataSource AuthenticationDataSource) { + return null; + } + @Override void close(); diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java index 9862b0a228386..dbb6598ff51ac 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java @@ -36,6 +36,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.pulsar.common.util.ObjectMapperFactory; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; @@ -188,23 +189,27 @@ public static List getCmd(InstanceConfig instanceConfig, args.add("--pulsar_serviceurl"); args.add(pulsarServiceUrl); if (authConfig != null) { - if (isNotBlank(authConfig.getClientAuthenticationPlugin()) - && isNotBlank(authConfig.getClientAuthenticationParameters())) { - args.add("--client_auth_plugin"); - args.add(authConfig.getClientAuthenticationPlugin()); - args.add("--client_auth_params"); - args.add(authConfig.getClientAuthenticationParameters()); - } - args.add("--use_tls"); - args.add(Boolean.toString(authConfig.isUseTls())); - args.add("--tls_allow_insecure"); - args.add(Boolean.toString(authConfig.isTlsAllowInsecureConnection())); - args.add("--hostname_verification_enabled"); - args.add(Boolean.toString(authConfig.isTlsHostnameVerificationEnable())); - if (isNotBlank(authConfig.getTlsTrustCertsFilePath())) { - args.add("--tls_trust_cert_path"); - args.add(authConfig.getTlsTrustCertsFilePath()); - } + + args.add("--auth_config"); + args.add(ObjectMapperFactory.getThreadLocal().writeValueAsString(authConfig)); + +// if (isNotBlank(authConfig.getClientAuthenticationPlugin()) +// && isNotBlank(authConfig.getClientAuthenticationParameters())) { +// args.add("--client_auth_plugin"); +// args.add(authConfig.getClientAuthenticationPlugin()); +// args.add("--client_auth_params"); +// args.add(authConfig.getClientAuthenticationParameters()); +// } +// args.add("--use_tls"); +// args.add(Boolean.toString(authConfig.isUseTls())); +// args.add("--tls_allow_insecure"); +// args.add(Boolean.toString(authConfig.isTlsAllowInsecureConnection())); +// args.add("--hostname_verification_enabled"); +// args.add(Boolean.toString(authConfig.isTlsHostnameVerificationEnable())); +// if (isNotBlank(authConfig.getTlsTrustCertsFilePath())) { +// args.add("--tls_trust_cert_path"); +// args.add(authConfig.getTlsTrustCertsFilePath()); +// } } args.add("--max_buffered_tuples"); args.add(String.valueOf(instanceConfig.getMaxBufferedTuples())); diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index 2f4856e3996a3..b4c22e99084fb 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -50,7 +50,6 @@ import java.util.function.Supplier; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; @@ -77,11 +76,11 @@ import static org.apache.pulsar.functions.utils.Utils.ComponentType.SOURCE; import org.apache.commons.lang3.StringUtils; +import org.apache.pulsar.broker.authentication.AuthenticationDataHttps; import org.apache.pulsar.client.admin.PulsarAdminException; import org.apache.pulsar.client.api.Message; import org.apache.pulsar.client.api.MessageId; import org.apache.pulsar.client.api.Producer; -import org.apache.pulsar.client.api.PulsarClientException; import org.apache.pulsar.client.api.Reader; import org.apache.pulsar.client.api.Schema; import org.apache.pulsar.common.functions.FunctionConfig; @@ -91,7 +90,6 @@ import org.apache.pulsar.common.io.SinkConfig; import org.apache.pulsar.common.io.SourceConfig; import org.apache.pulsar.common.naming.TopicName; -import org.apache.pulsar.common.policies.data.ErrorData; import org.apache.pulsar.common.policies.data.FunctionStats; import org.apache.pulsar.common.policies.data.TenantInfo; import org.apache.pulsar.common.util.Codec; @@ -294,7 +292,8 @@ public void registerFunction(final String tenant, final String functionPkgUrl, final String functionDetailsJson, final String componentConfigJson, - final String clientRole) { + final String clientRole, + AuthenticationDataHttps clientAuthenticationDataHttps) { if (!isWorkerServiceAvailable()) { throwUnavailableException(); @@ -377,9 +376,27 @@ public void registerFunction(final String tenant, throw new RestException(Status.BAD_REQUEST, String.format("%s %s cannot be admitted:- %s", componentType, componentName, e.getMessage())); } + // cache auth if need + try { + Function.FunctionAuthenticationSpec authenticationSpec = worker().getFunctionRuntimeManager() + .getRuntimeFactory() + .cacheAuthData(clientAuthenticationDataHttps); + + if (authenticationSpec != null) { + functionDetails = FunctionDetails.newBuilder(functionDetails) + .setFunctionAuthSpec(authenticationSpec) + .build(); + } + } catch (Exception e) { + log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName); + throw new RestException(Status.BAD_REQUEST, String.format("Error caching authentication data for %s %s:- %s", componentType, componentName, e.getMessage())); + } + // function state FunctionMetaData.Builder functionMetaDataBuilder = FunctionMetaData.newBuilder() - .setFunctionDetails(functionDetails).setCreateTime(System.currentTimeMillis()).setVersion(0); + .setFunctionDetails(functionDetails) + .setCreateTime(System.currentTimeMillis()) + .setVersion(0); PackageLocationMetaData.Builder packageLocationMetaDataBuilder; diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImplV2.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImplV2.java index c71e1c265bea2..45fbbf2d7e911 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImplV2.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/FunctionsImplV2.java @@ -90,7 +90,7 @@ public Response registerFunction(String tenant, String namespace, String functio uploadedInputStream, FormDataContentDisposition fileDetail, String functionPkgUrl, String functionDetailsJson, String functionConfigJson, String clientAppId) { delegate.registerFunction(tenant, namespace, functionName, uploadedInputStream, fileDetail, - functionPkgUrl, functionDetailsJson, functionConfigJson, clientAppId); + functionPkgUrl, functionDetailsJson, functionConfigJson, clientAppId, null); return Response.ok().build(); } diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java index 4d27134611e3c..edc7686c4e49b 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java @@ -69,7 +69,7 @@ public void registerFunction(final @PathParam("tenant") String tenant, final @FormDataParam("functionConfig") String functionConfigJson) { functions.registerFunction(tenant, namespace, functionName, uploadedInputStream, fileDetail, - functionPkgUrl, null, functionConfigJson, clientAppId()); + functionPkgUrl, null, functionConfigJson, clientAppId(), null); } diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java index ee7d1a4c91851..b67aec3bc3dd9 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java @@ -25,7 +25,6 @@ import org.apache.commons.lang.StringUtils; import org.apache.pulsar.common.io.ConnectorDefinition; import org.apache.pulsar.common.io.SinkConfig; -import org.apache.pulsar.common.policies.data.FunctionStatus; import org.apache.pulsar.common.policies.data.SinkStatus; import org.apache.pulsar.functions.worker.rest.FunctionApiResource; import org.apache.pulsar.functions.worker.rest.api.SinkImpl; @@ -34,7 +33,6 @@ import javax.ws.rs.*; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -62,7 +60,7 @@ public void registerSink(final @PathParam("tenant") String tenant, final @FormDataParam("sinkConfig") String sinkConfigJson) { sink.registerFunction(tenant, namespace, sinkName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sinkConfigJson, clientAppId()); + functionPkgUrl, null, sinkConfigJson, clientAppId(), null); } @PUT diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java index c532e3abdb258..23b29f82327a5 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java @@ -33,7 +33,6 @@ import javax.ws.rs.*; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -61,7 +60,7 @@ public void registerSource(final @PathParam("tenant") String tenant, final @FormDataParam("sourceConfig") String sourceConfigJson) { source.registerFunction(tenant, namespace, sourceName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sourceConfigJson, clientAppId()); + functionPkgUrl, null, sourceConfigJson, clientAppId(), null); } diff --git a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3ResourceTest.java b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3ResourceTest.java index 7695993114f9d..e02b25354814f 100644 --- a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3ResourceTest.java +++ b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3ResourceTest.java @@ -50,7 +50,6 @@ import org.apache.pulsar.functions.worker.WorkerService; import org.apache.pulsar.functions.worker.request.RequestResult; import org.apache.pulsar.functions.worker.rest.RestException; -import org.apache.pulsar.functions.worker.rest.api.ComponentImpl; import org.apache.pulsar.functions.worker.rest.api.FunctionsImpl; import org.apache.pulsar.functions.worker.rest.api.v2.FunctionApiV2Resource; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -488,7 +487,7 @@ private void testRegisterFunctionMissingArguments( functionPkgUrl, null, new Gson().toJson(functionConfig), - null); + null, null); } @@ -503,7 +502,7 @@ private void registerDefaultFunction() { null, null, new Gson().toJson(functionConfig), - null); + null, null); } @Test(expectedExceptions = RestException.class, expectedExceptionsMessageRegExp = "Function test-function already exists") @@ -1447,7 +1446,7 @@ public void testRegisterFunctionFileUrlWithValidSinkClass() { functionConfig.setOutput(outputTopic); functionConfig.setOutputSerdeClassName(outputSerdeClassName); resource.registerFunction(tenant, namespace, function, null, null, filePackageUrl, - null, new Gson().toJson(functionConfig), null); + null, new Gson().toJson(functionConfig), null, null); } @@ -1479,7 +1478,7 @@ public void testRegisterFunctionWithConflictingFields() { functionConfig.setOutput(outputTopic); functionConfig.setOutputSerdeClassName(outputSerdeClassName); resource.registerFunction(actualTenant, actualNamespace, actualName, null, null, filePackageUrl, - null, new Gson().toJson(functionConfig), null); + null, new Gson().toJson(functionConfig), null, null); } public static FunctionConfig createDefaultFunctionConfig() { diff --git a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3ResourceTest.java b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3ResourceTest.java index 9e558002168ab..1b475ed3a7cc6 100644 --- a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3ResourceTest.java +++ b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3ResourceTest.java @@ -420,7 +420,7 @@ private void testRegisterSinkMissingArguments( pkgUrl, null, new Gson().toJson(sinkConfig), - null); + null, null); } @@ -435,7 +435,7 @@ private void registerDefaultSink() throws IOException { null, null, new Gson().toJson(sinkConfig), - null); + null, null); } @Test(expectedExceptions = RestException.class, expectedExceptionsMessageRegExp = "Sink test-sink already exists") @@ -529,7 +529,7 @@ public void testRegisterSinkConflictingFields() throws Exception { null, null, new Gson().toJson(sinkConfig), - null); + null, null); } @Test(expectedExceptions = RestException.class, expectedExceptionsMessageRegExp = "sink failed to register") diff --git a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3ResourceTest.java b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3ResourceTest.java index 6ba89140ee1aa..43f33034ff664 100644 --- a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3ResourceTest.java +++ b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3ResourceTest.java @@ -389,7 +389,7 @@ private void testRegisterSourceMissingArguments( pkgUrl, null, new Gson().toJson(sourceConfig), - null); + null, null); } @@ -404,7 +404,7 @@ private void registerDefaultSource() throws IOException { null, null, new Gson().toJson(sourceConfig), - null); + null, null); } @Test(expectedExceptions = RestException.class, expectedExceptionsMessageRegExp = "Source test-source already exists") @@ -498,7 +498,7 @@ public void testRegisterSourceConflictingFields() throws Exception { null, null, new Gson().toJson(sourceConfig), - null); + null, null); } @Test(expectedExceptions = RestException.class, expectedExceptionsMessageRegExp = "source failed to register") From 1fabe0bc4b572cf7f9ae3561ccc6ecfc16b70e53 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Fri, 1 Mar 2019 17:55:23 -0800 Subject: [PATCH 02/22] delete unnecessary changes --- .../apache/pulsar/functions/instance/AuthenticationConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java index 864abaee564cb..f4c78eac66412 100644 --- a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java +++ b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/AuthenticationConfig.java @@ -35,7 +35,6 @@ @ToString @Builder public class AuthenticationConfig { - private String functionAuthProviderPlugin; private String clientAuthenticationPlugin; private String clientAuthenticationParameters; private String tlsTrustCertsFilePath; From 313bcbad713c990e4792d9883b9841aee0cfe41d Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Mon, 25 Feb 2019 15:42:50 -0800 Subject: [PATCH 03/22] cleaning up --- .../broker/authentication/AuthenticationDataSource.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java index 70cab841f6bf7..9fc6cbe532b3d 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationDataSource.java @@ -117,13 +117,4 @@ default boolean hasDataFromPeer() { default SocketAddress getPeerAddress() { return null; } - - - default boolean isAuthDataForwardable() { - return false; - } - - default byte[] getData() { - return new byte[0]; - } } From 027e9f72507bbf1b1b73036f5dc291e21f1d50ed Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Fri, 1 Mar 2019 17:56:26 -0800 Subject: [PATCH 04/22] improving implementation --- bin/pulsar-admin | 1 + conf/functions_worker.yml | 6 +- .../proto/src/main/proto/Function.proto | 10 +- .../ClearTextFunctionTokenAuthProvider.java | 87 ++++++ .../functions/auth/FunctionAuthProvider.java | 32 ++ .../auth/KubernetesFunctionAuthProvider.java | 30 ++ .../KubernetesSecretsTokenAuthProvider.java | 287 ++++++++++++++++++ .../functions/runtime/KubernetesRuntime.java | 253 ++++++++++++--- .../runtime/KubernetesRuntimeFactory.java | 96 ++---- .../runtime/ProcessRuntimeFactory.java | 6 + .../pulsar/functions/runtime/Runtime.java | 4 + .../functions/runtime/RuntimeFactory.java | 8 +- .../functions/runtime/RuntimeSpawner.java | 22 ++ .../functions/runtime/RuntimeUtils.java | 39 ++- .../functions/worker/FunctionActioner.java | 57 ++-- .../worker/rest/api/ComponentImpl.java | 47 +-- 16 files changed, 792 insertions(+), 193 deletions(-) create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java diff --git a/bin/pulsar-admin b/bin/pulsar-admin index 482a98d6ce813..97206e61f4cdc 100755 --- a/bin/pulsar-admin +++ b/bin/pulsar-admin @@ -17,6 +17,7 @@ # specific language governing permissions and limitations # under the License. # + BINDIR=$(dirname "$0") export PULSAR_HOME=`cd $BINDIR/..;pwd` . "$PULSAR_HOME/bin/pulsar-admin-common.sh" diff --git a/conf/functions_worker.yml b/conf/functions_worker.yml index 865da5b2325d6..0c1125d770352 100644 --- a/conf/functions_worker.yml +++ b/conf/functions_worker.yml @@ -44,12 +44,10 @@ downloadDirectory: /tmp/pulsar_functions # points pulsarServiceUrl: pulsar://localhost:6650 pulsarWebServiceUrl: http://localhost:8080 - # the authentication plugin to be used by the pulsar client used in worker service -clientAuthenticationPlugin: org.apache.pulsar.client.impl.auth.AuthenticationToken +# clientAuthenticationPlugin: # the authentication parameter to be used by the pulsar client used in worker service -clientAuthenticationParameters: file:///etc/auth/token - +# clientAuthenticationParameters: # pulsar topics used for function metadata management diff --git a/pulsar-functions/proto/src/main/proto/Function.proto b/pulsar-functions/proto/src/main/proto/Function.proto index 2bc5ea8581ef9..ca0b4f75cabe0 100644 --- a/pulsar-functions/proto/src/main/proto/Function.proto +++ b/pulsar-functions/proto/src/main/proto/Function.proto @@ -66,11 +66,6 @@ message FunctionDetails { Resources resources = 13; string packageUrl = 14; //present only if function submitted with package-url RetryDetails retryDetails = 15; - FunctionAuthenticationSpec functionAuthSpec = 17; -} - -message FunctionAuthenticationSpec { - string id = 1; } message ConsumerSpec { @@ -146,6 +141,11 @@ message FunctionMetaData { uint64 version = 3; uint64 createTime = 4; map instanceStates = 5; + FunctionAuthenticationSpec functionAuthSpec = 6; +} + +message FunctionAuthenticationSpec { + string data = 1; } message Instance { diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java new file mode 100644 index 0000000000000..d8b4d29ad558e --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import org.apache.commons.lang3.StringUtils; +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.client.impl.auth.AuthenticationToken; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.apache.pulsar.functions.proto.Function; + +import javax.naming.AuthenticationException; + +import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; +import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; + +public class ClearTextFunctionTokenAuthProvider implements FunctionAuthProvider { + @Override + public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec) { + authConfig.setClientAuthenticationPlugin(AuthenticationToken.class.getName()); + authConfig.setClientAuthenticationParameters("token://" + functionAuthenticationSpec.getData()); + } + + @Override + public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception { + String token = null; + try { + token = getToken(authenticationDataSource); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (token != null) { + return Function.FunctionAuthenticationSpec.newBuilder().setData(token).build(); + } + return null; + } + + @Override + public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec + functionAuthenticationSpec) throws Exception { + //no-op + } + + private String getToken(AuthenticationDataSource authData) throws AuthenticationException { + if (authData.hasDataFromCommand()) { + // Authenticate Pulsar binary connection + return authData.getCommandData(); + } else if (authData.hasDataFromHttp()) { + // Authentication HTTP request. The format here should be compliant to RFC-6750 + // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx + String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); + if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { + throw new AuthenticationException("Invalid HTTP Authorization header"); + } + + // Remove prefix + String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); + return validateToken(token); + } else { + return null; + } + } + + private String validateToken(final String token) throws AuthenticationException { + if (StringUtils.isNotBlank(token)) { + return token; + } else { + throw new AuthenticationException("Blank token found"); + } + } +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java new file mode 100644 index 0000000000000..73b8e1f4a1dcc --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.apache.pulsar.functions.proto.Function; + +public interface FunctionAuthProvider { + + void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec); + + Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception; + + void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec functionAuthenticationSpec) throws Exception; +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java new file mode 100644 index 0000000000000..9d070cad2f2bb --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import io.kubernetes.client.models.V1ServiceAccount; +import io.kubernetes.client.models.V1StatefulSet; +import org.apache.pulsar.functions.proto.Function; + +public interface KubernetesFunctionAuthProvider extends FunctionAuthProvider { + + void configureAuthDataKubernetesServiceAccount(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1ServiceAccount sa); + + void configureAuthDataStatefulSet(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1StatefulSet statefulSet); +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java new file mode 100644 index 0000000000000..bfd15b2f5380b --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -0,0 +1,287 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import io.kubernetes.client.ApiException; +import io.kubernetes.client.apis.CoreV1Api; +import io.kubernetes.client.models.V1DeleteOptions; +import io.kubernetes.client.models.V1ObjectMeta; +import io.kubernetes.client.models.V1ObjectReference; +import io.kubernetes.client.models.V1PodSpec; +import io.kubernetes.client.models.V1Secret; +import io.kubernetes.client.models.V1SecretVolumeSource; +import io.kubernetes.client.models.V1ServiceAccount; +import io.kubernetes.client.models.V1StatefulSet; +import io.kubernetes.client.models.V1Volume; +import io.kubernetes.client.models.V1VolumeMount; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.client.impl.auth.AuthenticationToken; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.apache.pulsar.functions.proto.Function; +import org.apache.pulsar.functions.runtime.RuntimeUtils; +import org.apache.pulsar.functions.utils.FunctionDetailsUtils; + +import javax.naming.AuthenticationException; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.net.HttpURLConnection.HTTP_CONFLICT; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; +import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; + +@Slf4j +public class KubernetesSecretsTokenAuthProvider implements KubernetesFunctionAuthProvider { + + private static final int NUM_RETRIES = 5; + private static final long SLEEP_BETWEEN_RETRIES_MS = 500; + private static final String SECRET_NAME = "function-auth"; + private static final String DEFAULT_SECRET_MOUNT_DIR = "/etc/auth"; + private static final String FUNCTION_AUTH_TOKEN = "token"; + + + private final CoreV1Api coreClient; + private final String kubeNamespace; + + public KubernetesSecretsTokenAuthProvider(CoreV1Api coreClient, String kubeNamespace) { + this.coreClient = coreClient; + this.kubeNamespace = kubeNamespace; + } + + @Override + public void configureAuthDataStatefulSet(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1StatefulSet statefulSet) { + + V1PodSpec podSpec = statefulSet.getSpec().getTemplate().getSpec(); + + // configure pod mount secret with auth token + Function.FunctionAuthenticationSpec authenticationSpec = functionAuthenticationSpec; + podSpec.setVolumes(Collections.singletonList( + new V1Volume() + .name(SECRET_NAME) + .secret( + new V1SecretVolumeSource() + .secretName(getSecretName(authenticationSpec.getData())) + .defaultMode(256)))); + + podSpec.getContainers().forEach(container -> container.setVolumeMounts(Collections.singletonList( + new V1VolumeMount() + .name(SECRET_NAME) + .mountPath(DEFAULT_SECRET_MOUNT_DIR) + .readOnly(true)))); + + } + + @Override + public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec) { + authConfig.setClientAuthenticationPlugin(AuthenticationToken.class.getName()); + authConfig.setClientAuthenticationParameters(String.format("file://%s/%s", DEFAULT_SECRET_MOUNT_DIR, FUNCTION_AUTH_TOKEN)); + } + + @Override + public void configureAuthDataKubernetesServiceAccount(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1ServiceAccount sa) { + sa.addSecretsItem(new V1ObjectReference().name("pf-secret-" + functionAuthenticationSpec.getData()).namespace(kubeNamespace)); + } + + @Override + public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) { + String id = null; + try { + String token = getToken(authenticationDataSource); + if (token != null) { + id = createSecret(token, tenant, namespace, name); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (id != null) { + return Function.FunctionAuthenticationSpec.newBuilder().setData(id).build(); + } + return null; + } + + @Override + public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec + functionAuthenticationSpec) throws Exception { + String fqfn = FunctionDetailsUtils.getFullyQualifiedName(tenant, namespace, name); + + String secretName = functionAuthenticationSpec.getData(); + RuntimeUtils.Actions.Action deleteSecrets = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Deleting secrets for function %s", fqfn)) + .numRetries(NUM_RETRIES) + .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + try { + V1DeleteOptions v1DeleteOptions = new V1DeleteOptions(); + v1DeleteOptions.setGracePeriodSeconds(0L); + v1DeleteOptions.setPropagationPolicy("Foreground"); + + coreClient.deleteNamespacedSecret(secretName, + kubeNamespace, v1DeleteOptions, "true", + null, null, null); + } catch (ApiException e) { + // if already deleted + if (e.getCode() == HTTP_NOT_FOUND) { + log.warn("Secrets for function {} does not exist", fqfn); + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + } + + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + }) + .build(); + + RuntimeUtils.Actions.Action waitForSecretsDeletion = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Waiting for secrets for function %s to complete deletion", fqfn)) + .numRetries(NUM_RETRIES) + .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + try { + coreClient.readNamespacedSecret(secretName, kubeNamespace, + null, null, null); + + } catch (ApiException e) { + // statefulset is gone + if (e.getCode() == HTTP_NOT_FOUND) { + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + } + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .build(); + }) + .build(); + + AtomicBoolean success = new AtomicBoolean(false); + RuntimeUtils.Actions.newBuilder() + .addAction(deleteSecrets.toBuilder() + .continueOn(true) + .build()) + .addAction(waitForSecretsDeletion.toBuilder() + .continueOn(false) + .onSuccess(() -> success.set(true)) + .build()) + .addAction(deleteSecrets.toBuilder() + .continueOn(true) + .build()) + .addAction(waitForSecretsDeletion.toBuilder() + .onSuccess(() -> success.set(true)) + .build()) + .run(); + + if (!success.get()) { + throw new RuntimeException(String.format("Failed to delete secrets for function %s", fqfn)); + } + } + + private String createSecret(String token, String tenant, String namespace, String name) throws ApiException, InterruptedException { + + StringBuilder sb = new StringBuilder(); + RuntimeUtils.Actions.Action createAuthSecret = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Creating authentication secret for function %s/%s/%s", tenant, namespace, name)) + .numRetries(NUM_RETRIES) + .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + String id = RandomStringUtils.random(5, true, true).toLowerCase(); + V1Secret v1Secret = new V1Secret() + .metadata(new V1ObjectMeta().name(getSecretName(id))) + .data(Collections.singletonMap(FUNCTION_AUTH_TOKEN, token.getBytes())); + try { + coreClient.createNamespacedSecret(kubeNamespace, v1Secret, "true"); + } catch (ApiException e) { + // already exists + if (e.getCode() == HTTP_CONFLICT) { + return RuntimeUtils.Actions.ActionResult.builder() + .errorMsg(String.format("Secret %s already present", id)) + .success(false) + .build(); + } + + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + + sb.append(id.toCharArray()); + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + }) + .build(); + + AtomicBoolean success = new AtomicBoolean(false); + RuntimeUtils.Actions.newBuilder() + .addAction(createAuthSecret.toBuilder() + .onSuccess(() -> success.set(true)) + .build()) + .run(); + + if (!success.get()) { + throw new RuntimeException(String.format("Failed to create authentication secret for function %s/%s/%s", tenant, namespace, name)); + } + + return sb.toString(); + } + + private String getSecretName(String id) { + return "pf-secret-" + id; + } + + private String getToken(AuthenticationDataSource authData) throws AuthenticationException { + if (authData.hasDataFromCommand()) { + // Authenticate Pulsar binary connection + return authData.getCommandData(); + } else if (authData.hasDataFromHttp()) { + // Authentication HTTP request. The format here should be compliant to RFC-6750 + // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx + String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); + if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { + throw new AuthenticationException("Invalid HTTP Authorization header"); + } + + // Remove prefix + String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); + return validateToken(token); + } else { + return null; + } + } + + private String validateToken(final String token) throws AuthenticationException { + if (StringUtils.isNotBlank(token)) { + return token; + } else { + throw new AuthenticationException("Blank token found"); + } + } +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java index 5fcd3a9ac13b6..cc17eb1d23093 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java @@ -44,17 +44,16 @@ import io.kubernetes.client.models.V1PodSpec; import io.kubernetes.client.models.V1PodTemplateSpec; import io.kubernetes.client.models.V1ResourceRequirements; -import io.kubernetes.client.models.V1SecretVolumeSource; import io.kubernetes.client.models.V1Service; +import io.kubernetes.client.models.V1ServiceAccount; import io.kubernetes.client.models.V1ServicePort; import io.kubernetes.client.models.V1ServiceSpec; import io.kubernetes.client.models.V1StatefulSet; import io.kubernetes.client.models.V1StatefulSetSpec; import io.kubernetes.client.models.V1Toleration; -import io.kubernetes.client.models.V1Volume; -import io.kubernetes.client.models.V1VolumeMount; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.pulsar.functions.auth.KubernetesFunctionAuthProvider; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.instance.InstanceUtils; @@ -82,6 +81,7 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Kubernetes based runtime for running functions. @@ -96,9 +96,6 @@ @VisibleForTesting public class KubernetesRuntime implements Runtime { - private static int NUM_RETRIES = 5; - private static long SLEEP_BETWEEN_RETRIES_MS = 500; - private static final String ENV_SHARD_ID = "SHARD_ID"; private static final int maxJobNameSize = 55; private static final Integer GRPC_PORT = 9093; @@ -136,7 +133,8 @@ public class KubernetesRuntime implements Runtime { private final String pulsarAdminUrl; private final SecretsProviderConfigurator secretsProviderConfigurator; private int percentMemoryPadding; - + private final KubernetesFunctionAuthProvider functionAuthDataCacheProvider; + private final AuthenticationConfig authConfig; KubernetesRuntime(AppsV1Api appsClient, CoreV1Api coreClient, @@ -160,7 +158,8 @@ public class KubernetesRuntime implements Runtime { AuthenticationConfig authConfig, SecretsProviderConfigurator secretsProviderConfigurator, Integer expectedMetricsCollectionInterval, - int percentMemoryPadding) throws Exception { + int percentMemoryPadding, + KubernetesFunctionAuthProvider functionAuthDataCacheProvider) throws Exception { this.appsClient = appsClient; this.coreClient = coreClient; this.instanceConfig = instanceConfig; @@ -189,11 +188,16 @@ public class KubernetesRuntime implements Runtime { break; } + this.authConfig = authConfig; + + this.functionAuthDataCacheProvider = functionAuthDataCacheProvider; + this.processArgs = new LinkedList<>(); this.processArgs.addAll(RuntimeUtils.getArgsBeforeCmd(instanceConfig, extraDependenciesDir)); // use exec to to launch function so that it gets launched in the foreground with the same PID as shell // so that when we kill the pod, the signal will get propagated to the function code this.processArgs.add("exec"); + this.processArgs.addAll( RuntimeUtils.getCmd( instanceConfig, @@ -223,16 +227,23 @@ public class KubernetesRuntime implements Runtime { */ @Override public void start() throws Exception { - submitService(); + try { + if (instanceConfig.getFunctionAuthenticationSpec() != null) { + createServiceAccount(); + } + submitService(); submitStatefulSet(); + } catch (Exception e) { - log.error("Could not submit statefulset for {}/{}/{}, deleting service as well", + log.error("Failed start function {}/{}/{} in Kubernetes", instanceConfig.getFunctionDetails().getTenant(), instanceConfig.getFunctionDetails().getNamespace(), instanceConfig.getFunctionDetails().getName(), e); - deleteService(); + stop(); + throw e; } + if (channel == null && stub == null) { channel = new ManagedChannel[instanceConfig.getFunctionDetails().getParallelism()]; stub = new InstanceControlGrpc.InstanceControlFutureStub[instanceConfig.getFunctionDetails().getParallelism()]; @@ -247,6 +258,66 @@ public void start() throws Exception { } } + private String generateServiceAccount(InstanceConfig instanceConfig) { + return instanceConfig.getFunctionDetails().getTenant() + + "-" + instanceConfig.getFunctionDetails().getNamespace() + + "-" + instanceConfig.getFunctionDetails().getName() + + "-" + instanceConfig.getFunctionAuthenticationSpec().getData(); + } + + private void createServiceAccount() throws ApiException, InterruptedException { + + String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); + Function.FunctionAuthenticationSpec authenticationSpec = instanceConfig.getFunctionAuthenticationSpec(); + String serviceAccountName = generateServiceAccount(instanceConfig); + V1ServiceAccount serviceAccount = new V1ServiceAccount() + .metadata( + new V1ObjectMeta() + .name(serviceAccountName) + .namespace(jobNamespace)); + + // configure service account for auth data if necessary + functionAuthDataCacheProvider.configureAuthDataKubernetesServiceAccount(authenticationSpec, serviceAccount); + + log.info("Creating service account with the following spec to k8 {} for function {}", appsClient.getApiClient().getJSON().serialize(serviceAccount), fqfn); + + RuntimeUtils.Actions.Action createServiceAccount = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Creating service account %s for function %s", serviceAccountName, fqfn)) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + try { + coreClient.createNamespacedServiceAccount(jobNamespace, serviceAccount, "true"); + } catch (ApiException e) { + // already exists + if (e.getCode() == HTTP_CONFLICT) { + log.warn("Service account {} already present for function {}", serviceAccountName, fqfn); + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + } + + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + }) + .build(); + + AtomicBoolean success = new AtomicBoolean(false); + RuntimeUtils.Actions.newBuilder() + .addAction(createServiceAccount.toBuilder() + .onSuccess(() -> success.set(true)) + .build()) + .run(); + + if (!success.get()) { + throw new RuntimeException(String.format("Failed to create service account %s for function %s", serviceAccountName, fqfn)); + } + } + @Override public void join() throws Exception { // K8 functions never return @@ -257,6 +328,7 @@ public void join() throws Exception { public void stop() throws Exception { deleteStatefulSet(); deleteService(); + deleteServiceAccounts(); if (channel != null) { for (ManagedChannel cn : channel) { @@ -365,8 +437,8 @@ private void submitService() throws Exception { RuntimeUtils.Actions.Action createService = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Submitting service for function %s", fqfn)) - .numRetries(NUM_RETRIES) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) .supplier(() -> { final V1Service response; try { @@ -430,6 +502,11 @@ private V1Service createService() { private void submitStatefulSet() throws Exception { final V1StatefulSet statefulSet = createStatefulSet(); + // Configure function authentication if needed + if (instanceConfig.getFunctionAuthenticationSpec() != null) { + functionAuthDataCacheProvider.configureAuthDataStatefulSet( + instanceConfig.getFunctionAuthenticationSpec(), statefulSet); + } log.info("Submitting the following spec to k8 {}", appsClient.getApiClient().getJSON().serialize(statefulSet)); @@ -437,8 +514,8 @@ private void submitStatefulSet() throws Exception { RuntimeUtils.Actions.Action createStatefulSet = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Submitting statefulset for function %s", fqfn)) - .numRetries(NUM_RETRIES) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) .supplier(() -> { final V1StatefulSet response; try { @@ -484,8 +561,8 @@ public void deleteStatefulSet() throws InterruptedException { String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); RuntimeUtils.Actions.Action deleteStatefulSet = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Deleting statefulset for function %s", fqfn)) - .numRetries(NUM_RETRIES) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) .supplier(() -> { Response response; try { @@ -533,8 +610,8 @@ public void deleteStatefulSet() throws InterruptedException { RuntimeUtils.Actions.Action waitForStatefulSetDeletion = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Waiting for statefulset for function %s to complete deletion", fqfn)) // set retry period to be about 2x the graceshutdown time - .numRetries(NUM_RETRIES * 2) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS* 2) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES * 2) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS* 2) .supplier(() -> { V1StatefulSet response; try { @@ -562,8 +639,8 @@ public void deleteStatefulSet() throws InterruptedException { // Need to wait for all pods to die so we can cleanup subscriptions. RuntimeUtils.Actions.Action waitForStatefulPodsToTerminate = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Waiting for pods for function %s to terminate", fqfn)) - .numRetries(NUM_RETRIES * 2) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS * 2) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES * 2) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS * 2) .supplier(() -> { String labels = String.format("tenant=%s,namespace=%s,name=%s", instanceConfig.getFunctionDetails().getTenant(), @@ -635,8 +712,8 @@ public void deleteService() throws InterruptedException { RuntimeUtils.Actions.Action deleteService = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Deleting service for function %s", fqfn)) - .numRetries(NUM_RETRIES) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) .supplier(() -> { final Response response; try { @@ -681,8 +758,8 @@ public void deleteService() throws InterruptedException { RuntimeUtils.Actions.Action waitForServiceDeletion = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Waiting for statefulset for function %s to complete deletion", fqfn)) - .numRetries(NUM_RETRIES) - .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) .supplier(() -> { V1Service response; try { @@ -729,6 +806,85 @@ public void deleteService() throws InterruptedException { } } + private void deleteServiceAccounts() throws InterruptedException { + String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); + + String serviceAccountName = generateServiceAccount(instanceConfig); + RuntimeUtils.Actions.Action deleteServiceAccount = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Deleting service account %s for function %s", serviceAccountName, fqfn)) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + try { + V1DeleteOptions v1DeleteOptions = new V1DeleteOptions(); + v1DeleteOptions.setGracePeriodSeconds(0L); + v1DeleteOptions.setPropagationPolicy("Foreground"); + + coreClient.deleteNamespacedServiceAccount(serviceAccountName, jobNamespace, + v1DeleteOptions, null, null, null, null); + } catch (ApiException e) { + // if already deleted + if (e.getCode() == HTTP_NOT_FOUND) { + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + } + + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + }) + .build(); + + RuntimeUtils.Actions.Action waitForServiceAccountDeletion = RuntimeUtils.Actions.Action.builder() + .actionName(String.format("Waiting for service account %s for function %s to complete deletion", serviceAccountName, fqfn)) + .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) + .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) + .supplier(() -> { + try { + coreClient.readNamespacedServiceAccount(serviceAccountName, jobNamespace, null, null, null); + + } catch (ApiException e) { + // statefulset is gone + if (e.getCode() == HTTP_NOT_FOUND) { + return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + } + String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .errorMsg(errorMsg) + .build(); + } + return RuntimeUtils.Actions.ActionResult.builder() + .success(false) + .build(); + }) + .build(); + + AtomicBoolean success = new AtomicBoolean(false); + RuntimeUtils.Actions.newBuilder() + .addAction(deleteServiceAccount.toBuilder() + .continueOn(true) + .build()) + .addAction(waitForServiceAccountDeletion.toBuilder() + .continueOn(false) + .onSuccess(() -> success.set(true)) + .build()) + .addAction(deleteServiceAccount.toBuilder() + .continueOn(true) + .build()) + .addAction(waitForServiceAccountDeletion.toBuilder() + .onSuccess(() -> success.set(true)) + .build()) + .run(); + + if (!success.get()) { + throw new RuntimeException(String.format("Failed to delete service account %s for function %s", serviceAccountName, fqfn)); + } + } + protected List getExecutorCommand() { return Arrays.asList( "sh", @@ -740,6 +896,28 @@ protected List getExecutorCommand() { } private List getDownloadCommand(String bkPath, String userCodeFilePath) { + + // add auth plugin and parameters if necessary + if (authConfig != null) { + if (isNotBlank(authConfig.getClientAuthenticationPlugin()) + && isNotBlank(authConfig.getClientAuthenticationParameters())) { + return Arrays.asList( + pulsarRootDir + "/bin/pulsar-admin", + "--auth-plugin", + authConfig.getClientAuthenticationPlugin(), + "--auth-params", + authConfig.getClientAuthenticationParameters(), + "--admin-url", + pulsarAdminUrl, + "functions", + "download", + "--path", + bkPath, + "--destination-file", + userCodeFilePath); + } + } + return Arrays.asList( pulsarRootDir + "/bin/pulsar-admin", "--admin-url", @@ -850,22 +1028,12 @@ private V1PodSpec getPodSpec(List instanceCommand, Function.Resources re containers.add(getFunctionContainer(instanceCommand, resource)); podSpec.containers(containers); - // set authentication token - Function.FunctionAuthenticationSpec authenticationSpec = instanceConfig.getFunctionDetails().getFunctionAuthSpec(); - if (authenticationSpec != null) { - podSpec.setVolumes(Collections.singletonList( - new V1Volume() - .name("function-auth") - .secret( - new V1SecretVolumeSource() - .secretName(authenticationSpec.getId()) - - .defaultMode(256)))); - } - // Configure secrets secretsProviderConfigurator.configureKubernetesRuntimeSecretsProvider(podSpec, PULSARFUNCTIONS_CONTAINER_NAME, instanceConfig.getFunctionDetails()); + log.info("Setting service account {} for function {}", generateServiceAccount(instanceConfig), FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails())); + podSpec.setServiceAccount(generateServiceAccount(instanceConfig)); + return podSpec; } @@ -922,15 +1090,6 @@ V1Container getFunctionContainer(List instanceCommand, Function.Resource // set container ports container.setPorts(getFunctionContainerPorts()); - // auth - if (instanceConfig.getFunctionDetails().getFunctionAuthSpec()) - container.setVolumeMounts(Collections.singletonList( - new V1VolumeMount() - .name("function-auth") - .mountPath(authPath) -// .mountPath("/etc/auth") - .readOnly(true))); - return container; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java index 939c63182617b..8d146d3484dd1 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java @@ -21,38 +21,32 @@ import com.google.common.annotations.VisibleForTesting; import io.kubernetes.client.ApiClient; -import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; import io.kubernetes.client.apis.AppsV1Api; import io.kubernetes.client.apis.CoreV1Api; import io.kubernetes.client.models.V1ConfigMap; -import io.kubernetes.client.models.V1ObjectMeta; -import io.kubernetes.client.models.V1Secret; import io.kubernetes.client.util.Config; import java.nio.file.Paths; + import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.common.functions.Resources; +import org.apache.pulsar.functions.auth.KubernetesFunctionAuthProvider; +import org.apache.pulsar.functions.auth.KubernetesSecretsTokenAuthProvider; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; import org.apache.pulsar.functions.secretsproviderconfigurator.SecretsProviderConfigurator; -import javax.naming.AuthenticationException; import java.lang.reflect.Field; -import java.util.Collections; import java.util.Map; import java.util.Timer; import java.util.TimerTask; -import java.util.UUID; import static org.apache.commons.lang3.StringUtils.isEmpty; -import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; -import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; /** * Kubernetes based function container factory implementation. @@ -60,6 +54,9 @@ @Slf4j public class KubernetesRuntimeFactory implements RuntimeFactory { + static int NUM_RETRIES = 5; + static long SLEEP_BETWEEN_RETRIES_MS = 500; + @Getter @Setter @NoArgsConstructor @@ -167,6 +164,11 @@ public KubernetesRuntimeFactory(String k8Uri, this.expectedMetricsCollectionInterval = expectedMetricsCollectionInterval == null ? -1 : expectedMetricsCollectionInterval; this.secretsProviderConfigurator = secretsProviderConfigurator; this.functionInstanceMinResources = functionInstanceMinResources; + try { + setupClient(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override @@ -178,7 +180,6 @@ public boolean externallyManaged() { public KubernetesRuntime createContainer(InstanceConfig instanceConfig, String codePkgUrl, String originalCodeFileName, Long expectedHealthCheckInterval) throws Exception { - setupClient(); String instanceFile; switch (instanceConfig.getFunctionDetails().getRuntime()) { case JAVA: @@ -190,6 +191,12 @@ public KubernetesRuntime createContainer(InstanceConfig instanceConfig, String c default: throw new RuntimeException("Unsupported Runtime " + instanceConfig.getFunctionDetails().getRuntime()); } + + // adjust the auth config to support auth + if (instanceConfig.getFunctionAuthenticationSpec() != null) { + getAuthProvider().configureAuthenticationConfig(authConfig, instanceConfig.getFunctionAuthenticationSpec()); + } + return new KubernetesRuntime( appsClient, coreClient, @@ -213,7 +220,8 @@ public KubernetesRuntime createContainer(InstanceConfig instanceConfig, String c authConfig, secretsProviderConfigurator, expectedMetricsCollectionInterval, - this.kubernetesInfo.getPercentMemoryPadding()); + this.kubernetesInfo.getPercentMemoryPadding(), + getAuthProvider()); } @Override @@ -224,11 +232,6 @@ public void close() { public void doAdmissionChecks(Function.FunctionDetails functionDetails) { KubernetesRuntime.doChecks(functionDetails); validateMinResourcesRequired(functionDetails); - try { - setupClient(); - } catch (Exception e) { - throw new RuntimeException(e); - } secretsProviderConfigurator.doAdmissionChecks(appsClient, coreClient, kubernetesInfo.getJobNamespace(), functionDetails); } @@ -320,64 +323,7 @@ void validateMinResourcesRequired(Function.FunctionDetails functionDetails) { } @Override - public Function.FunctionAuthenticationSpec cacheAuthData(AuthenticationDataSource authenticationDataSource) { - String id = null; - try { - String token = getToken(authenticationDataSource); - if (token != null) { - id = createSecret(token); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - - if (id != null) { - return Function.FunctionAuthenticationSpec.newBuilder().setId(id).build(); - } - return null; - } - - static final String FUNCTION_AUTH_TOKEN = "token"; - private String createSecret(String token) throws ApiException { - try { - setupClient(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - String id = UUID.randomUUID().toString(); - V1Secret v1Secret = new V1Secret() - .metadata(new V1ObjectMeta().name(id)) - .data(Collections.singletonMap(FUNCTION_AUTH_TOKEN, token.getBytes())); - coreClient.createNamespacedSecret(kubernetesInfo.getJobNamespace(), v1Secret, "true"); - return id; - } - - private String getToken(AuthenticationDataSource authData) throws AuthenticationException { - if (authData.hasDataFromCommand()) { - // Authenticate Pulsar binary connection - return authData.getCommandData(); - } else if (authData.hasDataFromHttp()) { - // Authentication HTTP request. The format here should be compliant to RFC-6750 - // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx - String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); - if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { - throw new AuthenticationException("Invalid HTTP Authorization header"); - } - - // Remove prefix - String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); - return validateToken(token); - } else { - return null; - } - } - - private String validateToken(final String token) throws AuthenticationException { - if (StringUtils.isNotBlank(token)) { - return token; - } else { - throw new AuthenticationException("Blank token found"); - } + public KubernetesFunctionAuthProvider getAuthProvider() { + return new KubernetesSecretsTokenAuthProvider(coreClient, kubernetesInfo.jobNamespace); } } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java index 2b7de1436d517..701437465fdd9 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java @@ -125,6 +125,12 @@ public ProcessRuntime createContainer(InstanceConfig instanceConfig, String code default: throw new RuntimeException("Unsupported Runtime " + instanceConfig.getFunctionDetails().getRuntime()); } + + // configure auth if necessary + if (instanceConfig.getFunctionAuthenticationSpec() != null) { + getAuthProvider().configureAuthenticationConfig(authConfig, instanceConfig.getFunctionAuthenticationSpec()); + } + return new ProcessRuntime( instanceConfig, instanceFile, diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/Runtime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/Runtime.java index 19a5fc9eeefd6..77f660f747fc2 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/Runtime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/Runtime.java @@ -35,6 +35,10 @@ public interface Runtime { void stop() throws Exception; + default void terminate() throws Exception { + stop(); + } + boolean isAlive(); Throwable getDeathException(); diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java index c47999335e69a..fe909a8c7d016 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java @@ -19,7 +19,8 @@ package org.apache.pulsar.functions.runtime; -import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.functions.auth.ClearTextFunctionTokenAuthProvider; +import org.apache.pulsar.functions.auth.FunctionAuthProvider; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; @@ -44,11 +45,12 @@ Runtime createContainer( default void doAdmissionChecks(Function.FunctionDetails functionDetails) { } - default Function.FunctionAuthenticationSpec cacheAuthData(AuthenticationDataSource AuthenticationDataSource) { - return null; + default FunctionAuthProvider getAuthProvider() throws IllegalAccessException, InstantiationException { + return ClearTextFunctionTokenAuthProvider.class.newInstance(); } @Override void close(); } + \ No newline at end of file diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java index aff3404f8616f..aeb56141228f3 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java @@ -139,8 +139,30 @@ public CompletableFuture getFunctionStatusAsJson(int instanceId) { }); } + /** + * Terminates the runtime + */ @Override public void close() { + // cancel liveness checker before terminating runtime. + if (processLivenessCheckTimer != null) { + processLivenessCheckTimer.cancel(true); + processLivenessCheckTimer = null; + } + if (null != runtime) { + try { + runtime.terminate(); + } catch (Exception e) { + log.warn("Failed to terminate function runtime: {}", e, e); + } + runtime = null; + } + } + + /** + * Stops the runtime + */ + public void stop() { // cancel liveness checker before stopping runtime. if (processLivenessCheckTimer != null) { processLivenessCheckTimer.cancel(true); diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java index dbb6598ff51ac..9862b0a228386 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java @@ -36,7 +36,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.pulsar.common.util.ObjectMapperFactory; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; @@ -189,27 +188,23 @@ public static List getCmd(InstanceConfig instanceConfig, args.add("--pulsar_serviceurl"); args.add(pulsarServiceUrl); if (authConfig != null) { - - args.add("--auth_config"); - args.add(ObjectMapperFactory.getThreadLocal().writeValueAsString(authConfig)); - -// if (isNotBlank(authConfig.getClientAuthenticationPlugin()) -// && isNotBlank(authConfig.getClientAuthenticationParameters())) { -// args.add("--client_auth_plugin"); -// args.add(authConfig.getClientAuthenticationPlugin()); -// args.add("--client_auth_params"); -// args.add(authConfig.getClientAuthenticationParameters()); -// } -// args.add("--use_tls"); -// args.add(Boolean.toString(authConfig.isUseTls())); -// args.add("--tls_allow_insecure"); -// args.add(Boolean.toString(authConfig.isTlsAllowInsecureConnection())); -// args.add("--hostname_verification_enabled"); -// args.add(Boolean.toString(authConfig.isTlsHostnameVerificationEnable())); -// if (isNotBlank(authConfig.getTlsTrustCertsFilePath())) { -// args.add("--tls_trust_cert_path"); -// args.add(authConfig.getTlsTrustCertsFilePath()); -// } + if (isNotBlank(authConfig.getClientAuthenticationPlugin()) + && isNotBlank(authConfig.getClientAuthenticationParameters())) { + args.add("--client_auth_plugin"); + args.add(authConfig.getClientAuthenticationPlugin()); + args.add("--client_auth_params"); + args.add(authConfig.getClientAuthenticationParameters()); + } + args.add("--use_tls"); + args.add(Boolean.toString(authConfig.isUseTls())); + args.add("--tls_allow_insecure"); + args.add(Boolean.toString(authConfig.isTlsAllowInsecureConnection())); + args.add("--hostname_verification_enabled"); + args.add(Boolean.toString(authConfig.isTlsHostnameVerificationEnable())); + if (isNotBlank(authConfig.getTlsTrustCertsFilePath())) { + args.add("--tls_trust_cert_path"); + args.add(authConfig.getTlsTrustCertsFilePath()); + } } args.add("--max_buffered_tuples"); args.add(String.valueOf(instanceConfig.getMaxBufferedTuples())); diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java index 1d1014e4c9d65..12a6777c81e9b 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java @@ -156,6 +156,7 @@ RuntimeSpawner getRuntimeSpawner(Function.Instance instance, String packageFile) FunctionDetails.Builder functionDetailsBuilder = FunctionDetails.newBuilder(functionMetaData.getFunctionDetails()); InstanceConfig instanceConfig = createInstanceConfig(functionDetailsBuilder.build(), + instance.getFunctionMetaData().getFunctionAuthSpec(), instanceId, workerConfig.getPulsarFunctionsCluster()); RuntimeSpawner runtimeSpawner = new RuntimeSpawner(instanceConfig, packageFile, @@ -166,7 +167,8 @@ RuntimeSpawner getRuntimeSpawner(Function.Instance instance, String packageFile) } - InstanceConfig createInstanceConfig(FunctionDetails functionDetails, int instanceId, String clusterName) { + InstanceConfig createInstanceConfig(FunctionDetails functionDetails, Function.FunctionAuthenticationSpec + functionAuthSpec, int instanceId, String clusterName) { InstanceConfig instanceConfig = new InstanceConfig(); instanceConfig.setFunctionDetails(functionDetails); // TODO: set correct function id and version when features implemented @@ -176,6 +178,7 @@ InstanceConfig createInstanceConfig(FunctionDetails functionDetails, int instanc instanceConfig.setMaxBufferedTuples(1024); instanceConfig.setPort(org.apache.pulsar.functions.utils.Utils.findAvailablePort()); instanceConfig.setClusterName(clusterName); + instanceConfig.setFunctionAuthenticationSpec(functionAuthSpec); return instanceConfig; } @@ -233,17 +236,9 @@ private void downloadFile(File pkgFile, boolean isPkgUrlProvided, FunctionMetaDa } } - public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { + private void cleanupFunctionFiles(FunctionRuntimeInfo functionRuntimeInfo) { Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); FunctionMetaData functionMetaData = instance.getFunctionMetaData(); - FunctionDetails details = functionMetaData.getFunctionDetails(); - log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), - instance.getInstanceId()); - if (functionRuntimeInfo.getRuntimeSpawner() != null) { - functionRuntimeInfo.getRuntimeSpawner().close(); - functionRuntimeInfo.setRuntimeSpawner(null); - } - // clean up function package File pkgDir = new File( workerConfig.getDownloadDirectory(), @@ -252,7 +247,7 @@ public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { if (pkgDir.exists()) { try { MoreFiles.deleteRecursively( - Paths.get(pkgDir.toURI()), RecursiveDeleteOption.ALLOW_INSECURE); + Paths.get(pkgDir.toURI()), RecursiveDeleteOption.ALLOW_INSECURE); } catch (IOException e) { log.warn("Failed to delete package for function: {}", FunctionDetailsUtils.getFullyQualifiedName(functionMetaData.getFunctionDetails()), e); @@ -260,14 +255,42 @@ public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { } } - public void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { - FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData() - .getFunctionDetails(); - log.info("{}/{}/{}-{} Terminating function...", details.getTenant(), details.getNamespace(), details.getName(), - functionRuntimeInfo.getFunctionInstance().getInstanceId()); + public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { + Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); + FunctionMetaData functionMetaData = instance.getFunctionMetaData(); + FunctionDetails details = functionMetaData.getFunctionDetails(); + log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), + instance.getInstanceId()); + if (functionRuntimeInfo.getRuntimeSpawner() != null) { + functionRuntimeInfo.getRuntimeSpawner().stop(); + functionRuntimeInfo.setRuntimeSpawner(null); + } + + cleanupFunctionFiles(functionRuntimeInfo); + } + + private void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { + FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionDetails(); String fqfn = FunctionDetailsUtils.getFullyQualifiedName(details); + log.info("{}-{} Terminating function...", fqfn,functionRuntimeInfo.getFunctionInstance().getInstanceId()); + + if (functionRuntimeInfo.getRuntimeSpawner() != null) { + functionRuntimeInfo.getRuntimeSpawner().close(); + // cleanup any auth data cached + try { + functionRuntimeInfo.getRuntimeSpawner() + .getRuntimeFactory().getAuthProvider() + .cleanUpAuthData( + details.getTenant(), details.getNamespace(), details.getName(), + functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionAuthSpec()); + } catch (Exception e) { + log.error("Failed to cleanup auth data for function: {}", fqfn, e); + } + functionRuntimeInfo.setRuntimeSpawner(null); + } + + cleanupFunctionFiles(functionRuntimeInfo); - stopFunction(functionRuntimeInfo); //cleanup subscriptions if (details.getSource().getCleanupSubscription()) { Map consumerSpecMap = details.getSource().getInputSpecsMap(); diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index b4c22e99084fb..ebaf9fb0f70b7 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -120,7 +120,8 @@ import net.jodah.typetools.TypeResolver; @Slf4j -public abstract class ComponentImpl { +public abstract class +ComponentImpl { private final AtomicReference storageClient = new AtomicReference<>(); protected final Supplier workerServiceSupplier; @@ -376,28 +377,29 @@ public void registerFunction(final String tenant, throw new RestException(Status.BAD_REQUEST, String.format("%s %s cannot be admitted:- %s", componentType, componentName, e.getMessage())); } - // cache auth if need - try { - Function.FunctionAuthenticationSpec authenticationSpec = worker().getFunctionRuntimeManager() - .getRuntimeFactory() - .cacheAuthData(clientAuthenticationDataHttps); - - if (authenticationSpec != null) { - functionDetails = FunctionDetails.newBuilder(functionDetails) - .setFunctionAuthSpec(authenticationSpec) - .build(); - } - } catch (Exception e) { - log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName); - throw new RestException(Status.BAD_REQUEST, String.format("Error caching authentication data for %s %s:- %s", componentType, componentName, e.getMessage())); - } - // function state FunctionMetaData.Builder functionMetaDataBuilder = FunctionMetaData.newBuilder() .setFunctionDetails(functionDetails) .setCreateTime(System.currentTimeMillis()) .setVersion(0); + // cache auth if need + if (clientAuthenticationDataHttps != null) { + try { + Function.FunctionAuthenticationSpec authenticationSpec = worker().getFunctionRuntimeManager() + .getRuntimeFactory() + .getAuthProvider() + .cacheAuthData(tenant, namespace, componentName, clientAuthenticationDataHttps); + + if (authenticationSpec != null) { + functionMetaDataBuilder.setFunctionAuthSpec(authenticationSpec).build(); + } + } catch (Exception e) { + log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName, e); + + throw new RestException(Status.INTERNAL_SERVER_ERROR, String.format("Error caching authentication data for %s %s:- %s", componentType, componentName, e.getMessage())); + } + } PackageLocationMetaData.Builder packageLocationMetaDataBuilder; try { @@ -513,6 +515,7 @@ public void updateFunction(final String tenant, String existingComponentConfigJson; FunctionMetaData existingComponent = functionMetaDataManager.getFunctionMetaData(tenant, namespace, componentName); + if (componentType.equals(FUNCTION)) { FunctionConfig existingFunctionConfig = FunctionConfigUtils.convertFromDetails(existingComponent.getFunctionDetails()); existingComponentConfigJson = new Gson().toJson(existingFunctionConfig); @@ -585,6 +588,9 @@ public void updateFunction(final String tenant, throw new RestException(Status.BAD_REQUEST, e.getMessage()); } + //merge new functiondetails with existing function details + functionDetails = existingComponent.getFunctionDetails().toBuilder().mergeFrom(functionDetails).build(); + try { worker().getFunctionRuntimeManager().getRuntimeFactory().doAdmissionChecks(functionDetails); } catch (Exception e) { @@ -592,9 +598,9 @@ public void updateFunction(final String tenant, throw new RestException(Status.BAD_REQUEST, String.format("%s %s cannot be admitted:- %s", componentType, componentName, e.getMessage())); } - // function state - FunctionMetaData.Builder functionMetaDataBuilder = FunctionMetaData.newBuilder() - .setFunctionDetails(functionDetails).setCreateTime(System.currentTimeMillis()).setVersion(0); + // merge from existing metadata + FunctionMetaData.Builder functionMetaDataBuilder = FunctionMetaData.newBuilder().mergeFrom(existingComponent) + .setFunctionDetails(functionDetails); PackageLocationMetaData.Builder packageLocationMetaDataBuilder; if (isNotBlank(functionPkgUrl) || uploadedInputStreamAsFile != null) { @@ -609,6 +615,7 @@ public void updateFunction(final String tenant, } functionMetaDataBuilder.setPackageLocation(packageLocationMetaDataBuilder); + updateRequest(functionMetaDataBuilder.build()); } From 3c7e5a2c9f12e70fbcb02c34822d487748ab0d90 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Sat, 2 Mar 2019 14:07:03 -0800 Subject: [PATCH 05/22] fixing tests --- .../pulsar/functions/instance/InstanceConfig.java | 2 ++ .../runtime/KubernetesRuntimeFactory.java | 2 +- .../runtime/KubernetesRuntimeFactoryTest.java | 2 -- .../pulsar/functions/worker/FunctionActioner.java | 2 +- .../functions/worker/FunctionRuntimeManager.java | 1 + .../worker/FunctionRuntimeManagerTest.java | 15 ++++++++++++--- 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/InstanceConfig.java b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/InstanceConfig.java index 51ec0d488c535..4d783e9f0d303 100644 --- a/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/InstanceConfig.java +++ b/pulsar-functions/instance/src/main/java/org/apache/pulsar/functions/instance/InstanceConfig.java @@ -23,6 +23,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.apache.pulsar.functions.proto.Function; import org.apache.pulsar.functions.proto.Function.FunctionDetails; /** @@ -40,6 +41,7 @@ public class InstanceConfig { private String functionVersion; private FunctionDetails functionDetails; private int maxBufferedTuples; + private Function.FunctionAuthenticationSpec functionAuthenticationSpec; private int port; private String clusterName; diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java index 8d146d3484dd1..402c2cbc785bb 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java @@ -236,7 +236,7 @@ public void doAdmissionChecks(Function.FunctionDetails functionDetails) { } @VisibleForTesting - void setupClient() throws Exception { + public void setupClient() throws Exception { if (appsClient == null) { if (this.kubernetesInfo.getK8Uri() == null) { log.info("k8Uri is null thus going by defaults"); diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactoryTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactoryTest.java index c97cdb623211d..8f1172e95a5ab 100644 --- a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactoryTest.java +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactoryTest.java @@ -165,8 +165,6 @@ public void testAdmissionChecks() throws Exception { factory = createKubernetesRuntimeFactory(null, null); FunctionDetails functionDetails = createFunctionDetails(); factory.doAdmissionChecks(functionDetails); - verify(factory, times(1)).setupClient(); - } @Test diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java index 12a6777c81e9b..e884afd77afc8 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java @@ -269,7 +269,7 @@ public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { cleanupFunctionFiles(functionRuntimeInfo); } - private void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { + public void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionDetails(); String fqfn = FunctionDetailsUtils.getFullyQualifiedName(details); log.info("{}-{} Terminating function...", fqfn,functionRuntimeInfo.getFunctionInstance().getInstanceId()); diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java index 3d550657f735b..37f8a9ab547ad 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java @@ -63,6 +63,7 @@ * This class managers all aspects of functions assignments and running of function assignments for this worker */ @Slf4j + public class FunctionRuntimeManager implements AutoCloseable{ // all assignments diff --git a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/FunctionRuntimeManagerTest.java b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/FunctionRuntimeManagerTest.java index a66ba907aad7b..91fec231d995f 100644 --- a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/FunctionRuntimeManagerTest.java +++ b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/FunctionRuntimeManagerTest.java @@ -48,7 +48,14 @@ import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @Slf4j public class FunctionRuntimeManagerTest { @@ -663,7 +670,9 @@ public boolean matches(Object o) { public void testExternallyManagedRuntimeUpdate() throws Exception { WorkerConfig workerConfig = new WorkerConfig(); workerConfig.setWorkerId("worker-1"); - workerConfig.setKubernetesContainerFactory(new WorkerConfig.KubernetesContainerFactory()); + workerConfig.setKubernetesContainerFactory( + new WorkerConfig.KubernetesContainerFactory() + .setSubmittingInsidePod(false)); workerConfig.setPulsarServiceUrl("pulsar://localhost:6650"); workerConfig.setStateStorageServiceUrl("foo"); workerConfig.setPulsarFunctionsCluster("cluster"); @@ -679,8 +688,8 @@ public void testExternallyManagedRuntimeUpdate() throws Exception { doReturn(pulsarClient).when(workerService).getClient(); doReturn(mock(PulsarAdmin.class)).when(workerService).getFunctionAdmin(); - KubernetesRuntimeFactory kubernetesRuntimeFactory = mock(KubernetesRuntimeFactory.class); + doNothing().when(kubernetesRuntimeFactory).setupClient(); doReturn(true).when(kubernetesRuntimeFactory).externallyManaged(); doReturn(mock(KubernetesRuntime.class)).when(kubernetesRuntimeFactory).createContainer(any(), any(), any(), any()); From 3ebaf54d1d5ab93840e77b4be14b44d17fadfe3d Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Sat, 2 Mar 2019 14:14:57 -0800 Subject: [PATCH 06/22] cleaning up --- .../pulsar/functions/worker/FunctionRuntimeManager.java | 1 - .../pulsar/functions/worker/rest/FunctionApiResource.java | 5 +++++ .../pulsar/functions/worker/rest/api/ComponentImpl.java | 3 +-- .../functions/worker/rest/api/v3/FunctionApiV3Resource.java | 2 +- .../functions/worker/rest/api/v3/SinkApiV3Resource.java | 2 +- .../functions/worker/rest/api/v3/SourceApiV3Resource.java | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java index 37f8a9ab547ad..3d550657f735b 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionRuntimeManager.java @@ -63,7 +63,6 @@ * This class managers all aspects of functions assignments and running of function assignments for this worker */ @Slf4j - public class FunctionRuntimeManager implements AutoCloseable{ // all assignments diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/FunctionApiResource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/FunctionApiResource.java index 4e6b34a6991ad..580072b68066d 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/FunctionApiResource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/FunctionApiResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.UriInfo; +import org.apache.pulsar.broker.authentication.AuthenticationDataHttps; import org.apache.pulsar.broker.web.AuthenticationFilter; import org.apache.pulsar.functions.worker.WorkerService; import org.apache.pulsar.functions.worker.rest.api.FunctionsImpl; @@ -53,4 +54,8 @@ public String clientAppId() { ? (String) httpRequest.getAttribute(AuthenticationFilter.AuthenticatedRoleAttributeName) : null; } + + public AuthenticationDataHttps clientAuthData() { + return (AuthenticationDataHttps) httpRequest.getAttribute(AuthenticationFilter.AuthenticatedDataAttributeName); + } } diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index ebaf9fb0f70b7..cf1edfb375b44 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -120,8 +120,7 @@ import net.jodah.typetools.TypeResolver; @Slf4j -public abstract class -ComponentImpl { +public abstract class ComponentImpl { private final AtomicReference storageClient = new AtomicReference<>(); protected final Supplier workerServiceSupplier; diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java index edc7686c4e49b..0415be29f5c5b 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/FunctionApiV3Resource.java @@ -69,7 +69,7 @@ public void registerFunction(final @PathParam("tenant") String tenant, final @FormDataParam("functionConfig") String functionConfigJson) { functions.registerFunction(tenant, namespace, functionName, uploadedInputStream, fileDetail, - functionPkgUrl, null, functionConfigJson, clientAppId(), null); + functionPkgUrl, null, functionConfigJson, clientAppId(), clientAuthData()); } diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java index b67aec3bc3dd9..e5d257f996591 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SinkApiV3Resource.java @@ -60,7 +60,7 @@ public void registerSink(final @PathParam("tenant") String tenant, final @FormDataParam("sinkConfig") String sinkConfigJson) { sink.registerFunction(tenant, namespace, sinkName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sinkConfigJson, clientAppId(), null); + functionPkgUrl, null, sinkConfigJson, clientAppId(), clientAuthData()); } @PUT diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java index 23b29f82327a5..e07bfe0fa2e1e 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/v3/SourceApiV3Resource.java @@ -60,7 +60,7 @@ public void registerSource(final @PathParam("tenant") String tenant, final @FormDataParam("sourceConfig") String sourceConfigJson) { source.registerFunction(tenant, namespace, sourceName, uploadedInputStream, fileDetail, - functionPkgUrl, null, sourceConfigJson, clientAppId(), null); + functionPkgUrl, null, sourceConfigJson, clientAppId(), clientAuthData()); } From 804d178ae1f65b6fdf50cec02359e32139f4cc9c Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Sun, 3 Mar 2019 21:54:12 -0800 Subject: [PATCH 07/22] add no op implementation --- .../auth/NoOpFunctionAuthProvider.java | 43 +++++++++++++++++++ .../functions/runtime/RuntimeFactory.java | 4 +- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java new file mode 100644 index 0000000000000..809c07b917c94 --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.apache.pulsar.functions.proto.Function; + +public class NoOpFunctionAuthProvider implements FunctionAuthProvider{ + @Override + public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec + functionAuthenticationSpec) { + + } + + @Override + public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) + throws Exception { + return null; + } + + @Override + public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec functionAuthenticationSpec) throws Exception { + + } +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java index fe909a8c7d016..4384f00856062 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeFactory.java @@ -19,8 +19,8 @@ package org.apache.pulsar.functions.runtime; -import org.apache.pulsar.functions.auth.ClearTextFunctionTokenAuthProvider; import org.apache.pulsar.functions.auth.FunctionAuthProvider; +import org.apache.pulsar.functions.auth.NoOpFunctionAuthProvider; import org.apache.pulsar.functions.instance.InstanceConfig; import org.apache.pulsar.functions.proto.Function; @@ -46,7 +46,7 @@ Runtime createContainer( default void doAdmissionChecks(Function.FunctionDetails functionDetails) { } default FunctionAuthProvider getAuthProvider() throws IllegalAccessException, InstantiationException { - return ClearTextFunctionTokenAuthProvider.class.newInstance(); + return NoOpFunctionAuthProvider.class.newInstance(); } @Override From 903aa818367c501198738b25b85ed0d0de9a8cd8 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Mon, 4 Mar 2019 10:43:36 -0800 Subject: [PATCH 08/22] cleaning up unnecessary changes --- .../functions/runtime/RuntimeSpawner.java | 22 --------- .../functions/worker/FunctionActioner.java | 48 ++++++------------- 2 files changed, 14 insertions(+), 56 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java index aeb56141228f3..aff3404f8616f 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeSpawner.java @@ -139,30 +139,8 @@ public CompletableFuture getFunctionStatusAsJson(int instanceId) { }); } - /** - * Terminates the runtime - */ @Override public void close() { - // cancel liveness checker before terminating runtime. - if (processLivenessCheckTimer != null) { - processLivenessCheckTimer.cancel(true); - processLivenessCheckTimer = null; - } - if (null != runtime) { - try { - runtime.terminate(); - } catch (Exception e) { - log.warn("Failed to terminate function runtime: {}", e, e); - } - runtime = null; - } - } - - /** - * Stops the runtime - */ - public void stop() { // cancel liveness checker before stopping runtime. if (processLivenessCheckTimer != null) { processLivenessCheckTimer.cancel(true); diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java index e884afd77afc8..0b456233d003d 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java @@ -236,9 +236,17 @@ private void downloadFile(File pkgFile, boolean isPkgUrlProvided, FunctionMetaDa } } - private void cleanupFunctionFiles(FunctionRuntimeInfo functionRuntimeInfo) { + public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); FunctionMetaData functionMetaData = instance.getFunctionMetaData(); + FunctionDetails details = functionMetaData.getFunctionDetails(); + log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), + instance.getInstanceId()); + if (functionRuntimeInfo.getRuntimeSpawner() != null) { + functionRuntimeInfo.getRuntimeSpawner().close(); + functionRuntimeInfo.setRuntimeSpawner(null); + } + // clean up function package File pkgDir = new File( workerConfig.getDownloadDirectory(), @@ -255,42 +263,14 @@ private void cleanupFunctionFiles(FunctionRuntimeInfo functionRuntimeInfo) { } } - public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { - Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); - FunctionMetaData functionMetaData = instance.getFunctionMetaData(); - FunctionDetails details = functionMetaData.getFunctionDetails(); - log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), - instance.getInstanceId()); - if (functionRuntimeInfo.getRuntimeSpawner() != null) { - functionRuntimeInfo.getRuntimeSpawner().stop(); - functionRuntimeInfo.setRuntimeSpawner(null); - } - - cleanupFunctionFiles(functionRuntimeInfo); - } - public void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { - FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionDetails(); + FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData() + .getFunctionDetails(); + log.info("{}/{}/{}-{} Terminating function...", details.getTenant(), details.getNamespace(), details.getName(), + functionRuntimeInfo.getFunctionInstance().getInstanceId()); String fqfn = FunctionDetailsUtils.getFullyQualifiedName(details); - log.info("{}-{} Terminating function...", fqfn,functionRuntimeInfo.getFunctionInstance().getInstanceId()); - - if (functionRuntimeInfo.getRuntimeSpawner() != null) { - functionRuntimeInfo.getRuntimeSpawner().close(); - // cleanup any auth data cached - try { - functionRuntimeInfo.getRuntimeSpawner() - .getRuntimeFactory().getAuthProvider() - .cleanUpAuthData( - details.getTenant(), details.getNamespace(), details.getName(), - functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionAuthSpec()); - } catch (Exception e) { - log.error("Failed to cleanup auth data for function: {}", fqfn, e); - } - functionRuntimeInfo.setRuntimeSpawner(null); - } - - cleanupFunctionFiles(functionRuntimeInfo); + stopFunction(functionRuntimeInfo); //cleanup subscriptions if (details.getSource().getCleanupSubscription()) { Map consumerSpecMap = details.getSource().getInputSpecsMap(); From 6a1a194f62d697f0bede141d55495f5da90f4d3f Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Mon, 4 Mar 2019 12:34:47 -0800 Subject: [PATCH 09/22] refactoring based on comments --- .../ClearTextFunctionTokenAuthProvider.java | 11 ++-- .../functions/auth/FunctionAuthData.java | 28 ++++++++++ .../functions/auth/FunctionAuthProvider.java | 28 ++++++++-- .../functions/auth/FunctionAuthUtils.java | 28 ++++++++++ .../auth/KubernetesFunctionAuthProvider.java | 15 ++++-- .../KubernetesSecretsTokenAuthProvider.java | 23 ++++---- .../auth/NoOpFunctionAuthProvider.java | 9 ++-- .../functions/runtime/KubernetesRuntime.java | 5 +- .../runtime/KubernetesRuntimeFactory.java | 4 +- .../runtime/ProcessRuntimeFactory.java | 4 +- .../functions/worker/FunctionActioner.java | 53 +++++++++++++------ .../worker/rest/api/ComponentImpl.java | 9 ++-- 12 files changed, 164 insertions(+), 53 deletions(-) create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java create mode 100644 pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java index d8b4d29ad558e..da1db63e7a55b 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -31,13 +31,13 @@ public class ClearTextFunctionTokenAuthProvider implements FunctionAuthProvider { @Override - public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec) { + public void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData) { authConfig.setClientAuthenticationPlugin(AuthenticationToken.class.getName()); - authConfig.setClientAuthenticationParameters("token://" + functionAuthenticationSpec.getData()); + authConfig.setClientAuthenticationParameters("token://" + functionAuthData.getData()); } @Override - public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception { + public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception { String token = null; try { token = getToken(authenticationDataSource); @@ -46,14 +46,13 @@ public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String n } if (token != null) { - return Function.FunctionAuthenticationSpec.newBuilder().setData(token).build(); + return FunctionAuthData.builder().data(token).build(); } return null; } @Override - public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec - functionAuthenticationSpec) throws Exception { + public void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception { //no-op } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java new file mode 100644 index 0000000000000..b61e3f387c54e --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class FunctionAuthData { + private String data; +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java index 73b8e1f4a1dcc..0e90353b0c3be 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java @@ -24,9 +24,31 @@ public interface FunctionAuthProvider { - void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec); + /** + * Set authentication configs for function instance based on the data in FunctionAuthenticationSpec + * @param authConfig authentication configs passed to the function instance + * @param functionAuthData function authentication data that is provider specific + */ + void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData); - Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception; + /** + * Cache auth data in as part of function metadata for function that runtime may need to configure authentication + * @param tenant tenant that the function is running under + * @param namespace namespace that is the function is running under + * @param name name of the function + * @param authenticationDataSource auth data + * @return + * @throws Exception + */ + FunctionAuthData cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception; - void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec functionAuthenticationSpec) throws Exception; + /** + * Clean up operation for auth when function is terminated + * @param tenant tenant that the function is running under + * @param namespace namespace that is the function is running under + * @param name name of the function + * @param functionAuthData function auth data + * @throws Exception + */ + void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java new file mode 100644 index 0000000000000..d0826d509ceaf --- /dev/null +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import org.apache.pulsar.functions.proto.Function; + +public final class FunctionAuthUtils { + + public static final FunctionAuthData getFunctionAuthData(Function.FunctionAuthenticationSpec functionAuthenticationSpec) { + return FunctionAuthData.builder().data(functionAuthenticationSpec.getData()).build(); + } +} diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java index 9d070cad2f2bb..6fc091ab64c7b 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java @@ -20,11 +20,20 @@ import io.kubernetes.client.models.V1ServiceAccount; import io.kubernetes.client.models.V1StatefulSet; -import org.apache.pulsar.functions.proto.Function; public interface KubernetesFunctionAuthProvider extends FunctionAuthProvider { - void configureAuthDataKubernetesServiceAccount(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1ServiceAccount sa); + /** + * Configure service account spec generated for function based on function auth data + * @param serviceAccount service account spec for the function + * @param functionAuthData function auth data + */ + void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAccount, FunctionAuthData functionAuthData); - void configureAuthDataStatefulSet(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1StatefulSet statefulSet); + /** + * Configure function statefulset spec based on function auth data + * @param statefulSet statefulset spec for function + * @param functionAuthData function auth data + */ + void configureAuthDataStatefulSet(V1StatefulSet statefulSet, FunctionAuthData functionAuthData); } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index bfd15b2f5380b..2c46c20867e58 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -36,7 +36,6 @@ import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.client.impl.auth.AuthenticationToken; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import org.apache.pulsar.functions.proto.Function; import org.apache.pulsar.functions.runtime.RuntimeUtils; import org.apache.pulsar.functions.utils.FunctionDetailsUtils; @@ -68,18 +67,17 @@ public KubernetesSecretsTokenAuthProvider(CoreV1Api coreClient, String kubeNames } @Override - public void configureAuthDataStatefulSet(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1StatefulSet statefulSet) { + public void configureAuthDataStatefulSet(V1StatefulSet statefulSet, FunctionAuthData functionAuthData) { V1PodSpec podSpec = statefulSet.getSpec().getTemplate().getSpec(); // configure pod mount secret with auth token - Function.FunctionAuthenticationSpec authenticationSpec = functionAuthenticationSpec; podSpec.setVolumes(Collections.singletonList( new V1Volume() .name(SECRET_NAME) .secret( new V1SecretVolumeSource() - .secretName(getSecretName(authenticationSpec.getData())) + .secretName(getSecretName(functionAuthData.getData())) .defaultMode(256)))); podSpec.getContainers().forEach(container -> container.setVolumeMounts(Collections.singletonList( @@ -91,19 +89,19 @@ public void configureAuthDataStatefulSet(Function.FunctionAuthenticationSpec fun } @Override - public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec) { + public void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData) { authConfig.setClientAuthenticationPlugin(AuthenticationToken.class.getName()); authConfig.setClientAuthenticationParameters(String.format("file://%s/%s", DEFAULT_SECRET_MOUNT_DIR, FUNCTION_AUTH_TOKEN)); } @Override - public void configureAuthDataKubernetesServiceAccount(Function.FunctionAuthenticationSpec functionAuthenticationSpec, V1ServiceAccount sa) { - sa.addSecretsItem(new V1ObjectReference().name("pf-secret-" + functionAuthenticationSpec.getData()).namespace(kubeNamespace)); + public void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAccount, FunctionAuthData functionAuthData) { + serviceAccount.addSecretsItem(new V1ObjectReference().name("pf-secret-" + functionAuthData.getData()).namespace(kubeNamespace)); } @Override - public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, - AuthenticationDataSource authenticationDataSource) { + public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) { String id = null; try { String token = getToken(authenticationDataSource); @@ -115,17 +113,16 @@ public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String n } if (id != null) { - return Function.FunctionAuthenticationSpec.newBuilder().setData(id).build(); + return FunctionAuthData.builder().data(id).build(); } return null; } @Override - public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec - functionAuthenticationSpec) throws Exception { + public void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception { String fqfn = FunctionDetailsUtils.getFullyQualifiedName(tenant, namespace, name); - String secretName = functionAuthenticationSpec.getData(); + String secretName = functionAuthData.getData(); RuntimeUtils.Actions.Action deleteSecrets = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Deleting secrets for function %s", fqfn)) .numRetries(NUM_RETRIES) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java index 809c07b917c94..337c0a9097580 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java @@ -24,20 +24,19 @@ public class NoOpFunctionAuthProvider implements FunctionAuthProvider{ @Override - public void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec - functionAuthenticationSpec) { + public void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData) { } @Override - public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, - AuthenticationDataSource authenticationDataSource) + public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) throws Exception { return null; } @Override - public void cleanUpAuthData(String tenant, String namespace, String name, Function.FunctionAuthenticationSpec functionAuthenticationSpec) throws Exception { + public void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception { } } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java index cc17eb1d23093..0ad7db80eb429 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java @@ -82,6 +82,7 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.pulsar.functions.auth.FunctionAuthUtils.getFunctionAuthData; /** * Kubernetes based runtime for running functions. @@ -277,7 +278,7 @@ private void createServiceAccount() throws ApiException, InterruptedException { .namespace(jobNamespace)); // configure service account for auth data if necessary - functionAuthDataCacheProvider.configureAuthDataKubernetesServiceAccount(authenticationSpec, serviceAccount); + functionAuthDataCacheProvider.configureAuthDataKubernetesServiceAccount(serviceAccount, getFunctionAuthData(authenticationSpec)); log.info("Creating service account with the following spec to k8 {} for function {}", appsClient.getApiClient().getJSON().serialize(serviceAccount), fqfn); @@ -505,7 +506,7 @@ private void submitStatefulSet() throws Exception { // Configure function authentication if needed if (instanceConfig.getFunctionAuthenticationSpec() != null) { functionAuthDataCacheProvider.configureAuthDataStatefulSet( - instanceConfig.getFunctionAuthenticationSpec(), statefulSet); + statefulSet, getFunctionAuthData(instanceConfig.getFunctionAuthenticationSpec())); } log.info("Submitting the following spec to k8 {}", appsClient.getApiClient().getJSON().serialize(statefulSet)); diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java index 402c2cbc785bb..7fa67ceaa0116 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntimeFactory.java @@ -47,6 +47,7 @@ import java.util.TimerTask; import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.pulsar.functions.auth.FunctionAuthUtils.getFunctionAuthData; /** * Kubernetes based function container factory implementation. @@ -167,6 +168,7 @@ public KubernetesRuntimeFactory(String k8Uri, try { setupClient(); } catch (Exception e) { + log.error("Failed to setup client", e); throw new RuntimeException(e); } } @@ -194,7 +196,7 @@ public KubernetesRuntime createContainer(InstanceConfig instanceConfig, String c // adjust the auth config to support auth if (instanceConfig.getFunctionAuthenticationSpec() != null) { - getAuthProvider().configureAuthenticationConfig(authConfig, instanceConfig.getFunctionAuthenticationSpec()); + getAuthProvider().configureAuthenticationConfig(authConfig, getFunctionAuthData(instanceConfig.getFunctionAuthenticationSpec())); } return new KubernetesRuntime( diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java index 701437465fdd9..0189264153904 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/ProcessRuntimeFactory.java @@ -29,6 +29,8 @@ import java.nio.file.Paths; +import static org.apache.pulsar.functions.auth.FunctionAuthUtils.getFunctionAuthData; + /** * Thread based function container factory implementation. */ @@ -128,7 +130,7 @@ public ProcessRuntime createContainer(InstanceConfig instanceConfig, String code // configure auth if necessary if (instanceConfig.getFunctionAuthenticationSpec() != null) { - getAuthProvider().configureAuthenticationConfig(authConfig, instanceConfig.getFunctionAuthenticationSpec()); + getAuthProvider().configureAuthenticationConfig(authConfig, getFunctionAuthData(instanceConfig.getFunctionAuthenticationSpec())); } return new ProcessRuntime( diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java index 0b456233d003d..afeb1421c6b68 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/FunctionActioner.java @@ -70,6 +70,7 @@ import static org.apache.pulsar.common.functions.Utils.FILE; import static org.apache.pulsar.common.functions.Utils.HTTP; import static org.apache.pulsar.common.functions.Utils.isFunctionPackageUrlSupported; +import static org.apache.pulsar.functions.auth.FunctionAuthUtils.getFunctionAuthData; import static org.apache.pulsar.functions.utils.Utils.getSinkType; import static org.apache.pulsar.functions.utils.Utils.getSourceType; @@ -236,17 +237,9 @@ private void downloadFile(File pkgFile, boolean isPkgUrlProvided, FunctionMetaDa } } - public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { + private void cleanupFunctionFiles(FunctionRuntimeInfo functionRuntimeInfo) { Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); FunctionMetaData functionMetaData = instance.getFunctionMetaData(); - FunctionDetails details = functionMetaData.getFunctionDetails(); - log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), - instance.getInstanceId()); - if (functionRuntimeInfo.getRuntimeSpawner() != null) { - functionRuntimeInfo.getRuntimeSpawner().close(); - functionRuntimeInfo.setRuntimeSpawner(null); - } - // clean up function package File pkgDir = new File( workerConfig.getDownloadDirectory(), @@ -263,14 +256,42 @@ public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { } } + public void stopFunction(FunctionRuntimeInfo functionRuntimeInfo) { + Function.Instance instance = functionRuntimeInfo.getFunctionInstance(); + FunctionMetaData functionMetaData = instance.getFunctionMetaData(); + FunctionDetails details = functionMetaData.getFunctionDetails(); + log.info("{}/{}/{}-{} Stopping function...", details.getTenant(), details.getNamespace(), details.getName(), + instance.getInstanceId()); + if (functionRuntimeInfo.getRuntimeSpawner() != null) { + functionRuntimeInfo.getRuntimeSpawner().close(); + functionRuntimeInfo.setRuntimeSpawner(null); + } + + cleanupFunctionFiles(functionRuntimeInfo); + } + public void terminateFunction(FunctionRuntimeInfo functionRuntimeInfo) { - FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData() - .getFunctionDetails(); - log.info("{}/{}/{}-{} Terminating function...", details.getTenant(), details.getNamespace(), details.getName(), - functionRuntimeInfo.getFunctionInstance().getInstanceId()); + FunctionDetails details = functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionDetails(); String fqfn = FunctionDetailsUtils.getFullyQualifiedName(details); + log.info("{}-{} Terminating function...", fqfn,functionRuntimeInfo.getFunctionInstance().getInstanceId()); + + if (functionRuntimeInfo.getRuntimeSpawner() != null) { + functionRuntimeInfo.getRuntimeSpawner().close(); + // cleanup any auth data cached + try { + functionRuntimeInfo.getRuntimeSpawner() + .getRuntimeFactory().getAuthProvider() + .cleanUpAuthData( + details.getTenant(), details.getNamespace(), details.getName(), + getFunctionAuthData(functionRuntimeInfo.getFunctionInstance().getFunctionMetaData().getFunctionAuthSpec())); + } catch (Exception e) { + log.error("Failed to cleanup auth data for function: {}", fqfn, e); + } + functionRuntimeInfo.setRuntimeSpawner(null); + } + + cleanupFunctionFiles(functionRuntimeInfo); - stopFunction(functionRuntimeInfo); //cleanup subscriptions if (details.getSource().getCleanupSubscription()) { Map consumerSpecMap = details.getSource().getInputSpecsMap(); @@ -314,8 +335,8 @@ public void accept(Map.Entry stringConsumerSpecEn SubscriptionStats sub = stats.subscriptions.get(InstanceUtils.getDefaultSubscriptionName(details)); if (sub != null) { existingConsumers = sub.consumers.stream() - .map(consumerStats -> consumerStats.metadata) - .collect(Collectors.toList()); + .map(consumerStats -> consumerStats.metadata) + .collect(Collectors.toList()); } } catch (PulsarAdminException e1) { diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index cf1edfb375b44..f653eab478f81 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -93,6 +93,7 @@ import org.apache.pulsar.common.policies.data.FunctionStats; import org.apache.pulsar.common.policies.data.TenantInfo; import org.apache.pulsar.common.util.Codec; +import org.apache.pulsar.functions.auth.FunctionAuthData; import org.apache.pulsar.functions.proto.Function; import org.apache.pulsar.functions.proto.Function.FunctionDetails; import org.apache.pulsar.functions.proto.Function.FunctionMetaData; @@ -385,13 +386,15 @@ public void registerFunction(final String tenant, // cache auth if need if (clientAuthenticationDataHttps != null) { try { - Function.FunctionAuthenticationSpec authenticationSpec = worker().getFunctionRuntimeManager() + FunctionAuthData functionAuthData = worker().getFunctionRuntimeManager() .getRuntimeFactory() .getAuthProvider() .cacheAuthData(tenant, namespace, componentName, clientAuthenticationDataHttps); - if (authenticationSpec != null) { - functionMetaDataBuilder.setFunctionAuthSpec(authenticationSpec).build(); + if (functionAuthData != null) { + functionMetaDataBuilder.setFunctionAuthSpec( + Function.FunctionAuthenticationSpec.newBuilder() + .setData(functionAuthData.getData())).build(); } } catch (Exception e) { log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName, e); From 814e50d596ebba04c01571f441f5f039580bff29 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Tue, 5 Mar 2019 12:43:58 -0800 Subject: [PATCH 10/22] adding comments --- .../org/apache/pulsar/functions/auth/FunctionAuthData.java | 3 +++ .../apache/pulsar/functions/auth/FunctionAuthProvider.java | 4 ++++ .../pulsar/functions/auth/KubernetesFunctionAuthProvider.java | 3 +++ 3 files changed, 10 insertions(+) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java index b61e3f387c54e..301e5f881e407 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java @@ -23,6 +23,9 @@ @Data @Builder +/** + * A wrapper for authentication data for functions + */ public class FunctionAuthData { private String data; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java index 0e90353b0c3be..6ee7dd3951601 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java @@ -22,6 +22,10 @@ import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.proto.Function; +/** + * This is a generic interface that functions can use to cache and distribute appropriate authentication + * data that is needed to configure the runtime of functions to support appropriate authentication of function instances + */ public interface FunctionAuthProvider { /** diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java index 6fc091ab64c7b..4ea229c942d0c 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java @@ -21,6 +21,9 @@ import io.kubernetes.client.models.V1ServiceAccount; import io.kubernetes.client.models.V1StatefulSet; +/** + * Kubernetes runtime specific functions authentication provider + */ public interface KubernetesFunctionAuthProvider extends FunctionAuthProvider { /** From 17f14c16c3fd7e8137b1c4764ee71f43b63e0fb6 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Tue, 5 Mar 2019 13:39:01 -0800 Subject: [PATCH 11/22] change data from string type to bytes --- .../functions/auth/ClearTextFunctionTokenAuthProvider.java | 2 +- .../org/apache/pulsar/functions/auth/FunctionAuthData.java | 2 +- .../org/apache/pulsar/functions/auth/FunctionAuthUtils.java | 2 +- .../functions/auth/KubernetesSecretsTokenAuthProvider.java | 6 +++--- .../pulsar/functions/worker/rest/api/ComponentImpl.java | 3 ++- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java index da1db63e7a55b..b85effa0b0858 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -46,7 +46,7 @@ public FunctionAuthData cacheAuthData(String tenant, String namespace, String na } if (token != null) { - return FunctionAuthData.builder().data(token).build(); + return FunctionAuthData.builder().data(token.getBytes()).build(); } return null; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java index 301e5f881e407..b41f03697823d 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java @@ -27,5 +27,5 @@ * A wrapper for authentication data for functions */ public class FunctionAuthData { - private String data; + private byte[] data; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java index d0826d509ceaf..33567063ccf6b 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthUtils.java @@ -23,6 +23,6 @@ public final class FunctionAuthUtils { public static final FunctionAuthData getFunctionAuthData(Function.FunctionAuthenticationSpec functionAuthenticationSpec) { - return FunctionAuthData.builder().data(functionAuthenticationSpec.getData()).build(); + return FunctionAuthData.builder().data(functionAuthenticationSpec.getData().toByteArray()).build(); } } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index 2c46c20867e58..61f7242e64172 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -77,7 +77,7 @@ public void configureAuthDataStatefulSet(V1StatefulSet statefulSet, FunctionAuth .name(SECRET_NAME) .secret( new V1SecretVolumeSource() - .secretName(getSecretName(functionAuthData.getData())) + .secretName(getSecretName(new String(functionAuthData.getData()))) .defaultMode(256)))); podSpec.getContainers().forEach(container -> container.setVolumeMounts(Collections.singletonList( @@ -113,7 +113,7 @@ public FunctionAuthData cacheAuthData(String tenant, String namespace, String na } if (id != null) { - return FunctionAuthData.builder().data(id).build(); + return FunctionAuthData.builder().data(id.getBytes()).build(); } return null; } @@ -122,7 +122,7 @@ public FunctionAuthData cacheAuthData(String tenant, String namespace, String na public void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception { String fqfn = FunctionDetailsUtils.getFullyQualifiedName(tenant, namespace, name); - String secretName = functionAuthData.getData(); + String secretName = new String(functionAuthData.getData()); RuntimeUtils.Actions.Action deleteSecrets = RuntimeUtils.Actions.Action.builder() .actionName(String.format("Deleting secrets for function %s", fqfn)) .numRetries(NUM_RETRIES) diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index f653eab478f81..b986c6fef9fd3 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -26,6 +26,7 @@ import com.google.gson.Gson; +import com.google.protobuf.ByteString; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; @@ -394,7 +395,7 @@ public void registerFunction(final String tenant, if (functionAuthData != null) { functionMetaDataBuilder.setFunctionAuthSpec( Function.FunctionAuthenticationSpec.newBuilder() - .setData(functionAuthData.getData())).build(); + .setData(ByteString.copyFrom(functionAuthData.getData())).build()); } } catch (Exception e) { log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName, e); From 59b437cd822e30cfe20e7a22d1250be4fced5c1f Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Tue, 5 Mar 2019 14:02:12 -0800 Subject: [PATCH 12/22] add proto file --- pulsar-functions/proto/src/main/proto/Function.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulsar-functions/proto/src/main/proto/Function.proto b/pulsar-functions/proto/src/main/proto/Function.proto index ca0b4f75cabe0..8843b74a84592 100644 --- a/pulsar-functions/proto/src/main/proto/Function.proto +++ b/pulsar-functions/proto/src/main/proto/Function.proto @@ -145,7 +145,7 @@ message FunctionMetaData { } message FunctionAuthenticationSpec { - string data = 1; + bytes data = 1; } message Instance { From 9e71238f351d6e39ff00541c4a28515e522b8dab Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 14:42:01 -0700 Subject: [PATCH 13/22] addressing comments --- .../auth/ClearTextFunctionTokenAuthProvider.java | 7 ++++--- .../pulsar/functions/auth/FunctionAuthProvider.java | 5 +++-- .../auth/KubernetesSecretsTokenAuthProvider.java | 9 +++++---- .../functions/auth/NoOpFunctionAuthProvider.java | 9 +++++---- .../functions/worker/rest/api/ComponentImpl.java | 10 ++++------ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java index b85effa0b0858..1c7b4e5b99317 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -22,10 +22,11 @@ import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.client.impl.auth.AuthenticationToken; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import org.apache.pulsar.functions.proto.Function; import javax.naming.AuthenticationException; +import java.util.Optional; + import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; @@ -37,7 +38,7 @@ public void configureAuthenticationConfig(AuthenticationConfig authConfig, Funct } @Override - public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception { + public Optional cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception { String token = null; try { token = getToken(authenticationDataSource); @@ -46,7 +47,7 @@ public FunctionAuthData cacheAuthData(String tenant, String namespace, String na } if (token != null) { - return FunctionAuthData.builder().data(token.getBytes()).build(); + return Optional.of(FunctionAuthData.builder().data(token.getBytes()).build()); } return null; } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java index 6ee7dd3951601..e30e9ade0c75c 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthProvider.java @@ -20,7 +20,8 @@ import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import org.apache.pulsar.functions.proto.Function; + +import java.util.Optional; /** * This is a generic interface that functions can use to cache and distribute appropriate authentication @@ -44,7 +45,7 @@ public interface FunctionAuthProvider { * @return * @throws Exception */ - FunctionAuthData cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception; + Optional cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception; /** * Clean up operation for auth when function is terminated diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index 61f7242e64172..ebe078360bdc2 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -41,6 +41,7 @@ import javax.naming.AuthenticationException; import java.util.Collections; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import static java.net.HttpURLConnection.HTTP_CONFLICT; @@ -100,8 +101,8 @@ public void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAc } @Override - public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, - AuthenticationDataSource authenticationDataSource) { + public Optional cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) { String id = null; try { String token = getToken(authenticationDataSource); @@ -113,9 +114,9 @@ public FunctionAuthData cacheAuthData(String tenant, String namespace, String na } if (id != null) { - return FunctionAuthData.builder().data(id.getBytes()).build(); + return Optional.of(FunctionAuthData.builder().data(id.getBytes()).build()); } - return null; + return Optional.empty(); } @Override diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java index 337c0a9097580..5b4a0ab1e3846 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/NoOpFunctionAuthProvider.java @@ -20,7 +20,8 @@ import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import org.apache.pulsar.functions.proto.Function; + +import java.util.Optional; public class NoOpFunctionAuthProvider implements FunctionAuthProvider{ @Override @@ -29,10 +30,10 @@ public void configureAuthenticationConfig(AuthenticationConfig authConfig, Funct } @Override - public FunctionAuthData cacheAuthData(String tenant, String namespace, String name, - AuthenticationDataSource authenticationDataSource) + public Optional cacheAuthData(String tenant, String namespace, String name, + AuthenticationDataSource authenticationDataSource) throws Exception { - return null; + return Optional.empty(); } @Override diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index b986c6fef9fd3..0a16cd5169d8c 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -44,6 +44,7 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -387,15 +388,15 @@ public void registerFunction(final String tenant, // cache auth if need if (clientAuthenticationDataHttps != null) { try { - FunctionAuthData functionAuthData = worker().getFunctionRuntimeManager() + Optional functionAuthData = worker().getFunctionRuntimeManager() .getRuntimeFactory() .getAuthProvider() .cacheAuthData(tenant, namespace, componentName, clientAuthenticationDataHttps); - if (functionAuthData != null) { + if (functionAuthData.isPresent()) { functionMetaDataBuilder.setFunctionAuthSpec( Function.FunctionAuthenticationSpec.newBuilder() - .setData(ByteString.copyFrom(functionAuthData.getData())).build()); + .setData(ByteString.copyFrom(functionAuthData.get().getData())).build()); } } catch (Exception e) { log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName, e); @@ -591,9 +592,6 @@ public void updateFunction(final String tenant, throw new RestException(Status.BAD_REQUEST, e.getMessage()); } - //merge new functiondetails with existing function details - functionDetails = existingComponent.getFunctionDetails().toBuilder().mergeFrom(functionDetails).build(); - try { worker().getFunctionRuntimeManager().getRuntimeFactory().doAdmissionChecks(functionDetails); } catch (Exception e) { From 7df641b532200680e70d49350eb0b28383505a46 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 15:26:25 -0700 Subject: [PATCH 14/22] up merging --- .../KubernetesSecretsTokenAuthProvider.java | 35 ++++++++++--------- .../functions/runtime/KubernetesRuntime.java | 34 +++++++++--------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index ebe078360bdc2..c7fc75e4ded9c 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -37,6 +37,7 @@ import org.apache.pulsar.client.impl.auth.AuthenticationToken; import org.apache.pulsar.functions.instance.AuthenticationConfig; import org.apache.pulsar.functions.runtime.RuntimeUtils; +import org.apache.pulsar.functions.utils.Actions; import org.apache.pulsar.functions.utils.FunctionDetailsUtils; import javax.naming.AuthenticationException; @@ -124,7 +125,7 @@ public void cleanUpAuthData(String tenant, String namespace, String name, Functi String fqfn = FunctionDetailsUtils.getFullyQualifiedName(tenant, namespace, name); String secretName = new String(functionAuthData.getData()); - RuntimeUtils.Actions.Action deleteSecrets = RuntimeUtils.Actions.Action.builder() + Actions.Action deleteSecrets = Actions.Action.builder() .actionName(String.format("Deleting secrets for function %s", fqfn)) .numRetries(NUM_RETRIES) .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) @@ -141,20 +142,20 @@ public void cleanUpAuthData(String tenant, String namespace, String name, Functi // if already deleted if (e.getCode() == HTTP_NOT_FOUND) { log.warn("Secrets for function {} does not exist", fqfn); - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); }) .build(); - RuntimeUtils.Actions.Action waitForSecretsDeletion = RuntimeUtils.Actions.Action.builder() + Actions.Action waitForSecretsDeletion = Actions.Action.builder() .actionName(String.format("Waiting for secrets for function %s to complete deletion", fqfn)) .numRetries(NUM_RETRIES) .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) @@ -166,34 +167,34 @@ public void cleanUpAuthData(String tenant, String namespace, String name, Functi } catch (ApiException e) { // statefulset is gone if (e.getCode() == HTTP_NOT_FOUND) { - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .build(); }) .build(); AtomicBoolean success = new AtomicBoolean(false); - RuntimeUtils.Actions.newBuilder() + Actions.newBuilder() .addAction(deleteSecrets.toBuilder() .continueOn(true) .build()) .addAction(waitForSecretsDeletion.toBuilder() .continueOn(false) - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .addAction(deleteSecrets.toBuilder() .continueOn(true) .build()) .addAction(waitForSecretsDeletion.toBuilder() - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .run(); @@ -205,7 +206,7 @@ public void cleanUpAuthData(String tenant, String namespace, String name, Functi private String createSecret(String token, String tenant, String namespace, String name) throws ApiException, InterruptedException { StringBuilder sb = new StringBuilder(); - RuntimeUtils.Actions.Action createAuthSecret = RuntimeUtils.Actions.Action.builder() + Actions.Action createAuthSecret = Actions.Action.builder() .actionName(String.format("Creating authentication secret for function %s/%s/%s", tenant, namespace, name)) .numRetries(NUM_RETRIES) .sleepBetweenInvocationsMs(SLEEP_BETWEEN_RETRIES_MS) @@ -219,28 +220,28 @@ private String createSecret(String token, String tenant, String namespace, Strin } catch (ApiException e) { // already exists if (e.getCode() == HTTP_CONFLICT) { - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .errorMsg(String.format("Secret %s already present", id)) .success(false) .build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } sb.append(id.toCharArray()); - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); }) .build(); AtomicBoolean success = new AtomicBoolean(false); - RuntimeUtils.Actions.newBuilder() + Actions.newBuilder() .addAction(createAuthSecret.toBuilder() - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .run(); diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java index 092d5e3508a82..523d649480b8d 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java @@ -283,7 +283,7 @@ private void createServiceAccount() throws ApiException, InterruptedException { log.info("Creating service account with the following spec to k8 {} for function {}", appsClient.getApiClient().getJSON().serialize(serviceAccount), fqfn); - RuntimeUtils.Actions.Action createServiceAccount = RuntimeUtils.Actions.Action.builder() + Actions.Action createServiceAccount = Actions.Action.builder() .actionName(String.format("Creating service account %s for function %s", serviceAccountName, fqfn)) .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) @@ -294,24 +294,24 @@ private void createServiceAccount() throws ApiException, InterruptedException { // already exists if (e.getCode() == HTTP_CONFLICT) { log.warn("Service account {} already present for function {}", serviceAccountName, fqfn); - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); }) .build(); AtomicBoolean success = new AtomicBoolean(false); - RuntimeUtils.Actions.newBuilder() + Actions.newBuilder() .addAction(createServiceAccount.toBuilder() - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .run(); @@ -812,7 +812,7 @@ private void deleteServiceAccounts() throws InterruptedException { String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); String serviceAccountName = generateServiceAccount(instanceConfig); - RuntimeUtils.Actions.Action deleteServiceAccount = RuntimeUtils.Actions.Action.builder() + Actions.Action deleteServiceAccount = Actions.Action.builder() .actionName(String.format("Deleting service account %s for function %s", serviceAccountName, fqfn)) .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) @@ -827,20 +827,20 @@ private void deleteServiceAccounts() throws InterruptedException { } catch (ApiException e) { // if already deleted if (e.getCode() == HTTP_NOT_FOUND) { - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); }) .build(); - RuntimeUtils.Actions.Action waitForServiceAccountDeletion = RuntimeUtils.Actions.Action.builder() + Actions.Action waitForServiceAccountDeletion = Actions.Action.builder() .actionName(String.format("Waiting for service account %s for function %s to complete deletion", serviceAccountName, fqfn)) .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) @@ -851,34 +851,34 @@ private void deleteServiceAccounts() throws InterruptedException { } catch (ApiException e) { // statefulset is gone if (e.getCode() == HTTP_NOT_FOUND) { - return RuntimeUtils.Actions.ActionResult.builder().success(true).build(); + return Actions.ActionResult.builder().success(true).build(); } String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .errorMsg(errorMsg) .build(); } - return RuntimeUtils.Actions.ActionResult.builder() + return Actions.ActionResult.builder() .success(false) .build(); }) .build(); AtomicBoolean success = new AtomicBoolean(false); - RuntimeUtils.Actions.newBuilder() + Actions.newBuilder() .addAction(deleteServiceAccount.toBuilder() .continueOn(true) .build()) .addAction(waitForServiceAccountDeletion.toBuilder() .continueOn(false) - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .addAction(deleteServiceAccount.toBuilder() .continueOn(true) .build()) .addAction(waitForServiceAccountDeletion.toBuilder() - .onSuccess(() -> success.set(true)) + .onSuccess(ignore -> success.set(true)) .build()) .run(); From d9f42fc774f0d70ce241df4f502b1e80116d0cfa Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 16:14:14 -0700 Subject: [PATCH 15/22] refactoring get token code --- .../AuthenticationProvider.java | 1 - .../AuthenticationProviderToken.java | 5 ++-- .../ClearTextFunctionTokenAuthProvider.java | 29 +------------------ .../KubernetesSecretsTokenAuthProvider.java | 29 +------------------ 4 files changed, 5 insertions(+), 59 deletions(-) diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java index 755fe8643857e..dd6092aed12f4 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java @@ -72,5 +72,4 @@ default AuthenticationState newAuthState(AuthData authData, throws AuthenticationException{ return new OneStageAuthenticationState(authData, remoteAddress, sslSession, this); } - } diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java index 78fd158e74d71..af394bd381507 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.security.Key; +import java.util.Optional; import javax.naming.AuthenticationException; @@ -76,7 +77,7 @@ public String authenticate(AuthenticationDataSource authData) throws Authenticat return parseToken(token); } - private String getToken(AuthenticationDataSource authData) throws AuthenticationException { + public static String getToken(AuthenticationDataSource authData) throws AuthenticationException { if (authData.hasDataFromCommand()) { // Authenticate Pulsar binary connection return authData.getCommandData(); @@ -96,7 +97,7 @@ private String getToken(AuthenticationDataSource authData) throws Authentication } } - private String validateToken(final String token) throws AuthenticationException { + private static String validateToken(final String token) throws AuthenticationException { if (StringUtils.isNotBlank(token)) { return token; } else { diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java index 1c7b4e5b99317..5603e7f3739ea 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -28,6 +28,7 @@ import java.util.Optional; import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; +import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.getToken; import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; public class ClearTextFunctionTokenAuthProvider implements FunctionAuthProvider { @@ -56,32 +57,4 @@ public Optional cacheAuthData(String tenant, String namespace, public void cleanUpAuthData(String tenant, String namespace, String name, FunctionAuthData functionAuthData) throws Exception { //no-op } - - private String getToken(AuthenticationDataSource authData) throws AuthenticationException { - if (authData.hasDataFromCommand()) { - // Authenticate Pulsar binary connection - return authData.getCommandData(); - } else if (authData.hasDataFromHttp()) { - // Authentication HTTP request. The format here should be compliant to RFC-6750 - // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx - String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); - if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { - throw new AuthenticationException("Invalid HTTP Authorization header"); - } - - // Remove prefix - String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); - return validateToken(token); - } else { - return null; - } - } - - private String validateToken(final String token) throws AuthenticationException { - if (StringUtils.isNotBlank(token)) { - return token; - } else { - throw new AuthenticationException("Blank token found"); - } - } } diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index c7fc75e4ded9c..596f23d65814d 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -48,6 +48,7 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; +import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.getToken; import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; @Slf4j @@ -255,32 +256,4 @@ private String createSecret(String token, String tenant, String namespace, Strin private String getSecretName(String id) { return "pf-secret-" + id; } - - private String getToken(AuthenticationDataSource authData) throws AuthenticationException { - if (authData.hasDataFromCommand()) { - // Authenticate Pulsar binary connection - return authData.getCommandData(); - } else if (authData.hasDataFromHttp()) { - // Authentication HTTP request. The format here should be compliant to RFC-6750 - // (https://tools.ietf.org/html/rfc6750#section-2.1). Eg: Authorization: Bearer xxxxxxxxxxxxx - String httpHeaderValue = authData.getHttpHeader(HTTP_HEADER_NAME); - if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) { - throw new AuthenticationException("Invalid HTTP Authorization header"); - } - - // Remove prefix - String token = httpHeaderValue.substring(HTTP_HEADER_VALUE_PREFIX.length()); - return validateToken(token); - } else { - return null; - } - } - - private String validateToken(final String token) throws AuthenticationException { - if (StringUtils.isNotBlank(token)) { - return token; - } else { - throw new AuthenticationException("Blank token found"); - } - } } From 5a3a621bad39873fc9f929faacfde46ba41efc7d Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 16:15:56 -0700 Subject: [PATCH 16/22] cleaning up --- .../pulsar/broker/authentication/AuthenticationProvider.java | 1 + .../broker/authentication/AuthenticationProviderToken.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java index dd6092aed12f4..755fe8643857e 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java @@ -72,4 +72,5 @@ default AuthenticationState newAuthState(AuthData authData, throws AuthenticationException{ return new OneStageAuthenticationState(authData, remoteAddress, sslSession, this); } + } diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java index af394bd381507..d55dcfe7a41f7 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java @@ -36,7 +36,7 @@ public class AuthenticationProviderToken implements AuthenticationProvider { final static String HTTP_HEADER_NAME = "Authorization"; - public final static String HTTP_HEADER_VALUE_PREFIX = "Bearer "; + final static String HTTP_HEADER_VALUE_PREFIX = "Bearer "; // When symmetric key is configured final static String CONF_TOKEN_SECRET_KEY = "tokenSecretKey"; From 9b390c5cbea4e1b9cb7a2f4fa718ba1a81b654a7 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 17:45:05 -0700 Subject: [PATCH 17/22] fix bugs and add tests --- .../broker/authentication/AuthenticationProviderToken.java | 1 - .../functions/auth/ClearTextFunctionTokenAuthProvider.java | 7 +------ .../functions/auth/KubernetesSecretsTokenAuthProvider.java | 7 +------ 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java index d55dcfe7a41f7..f5ee57cdcd1b7 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.security.Key; -import java.util.Optional; import javax.naming.AuthenticationException; diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java index 5603e7f3739ea..7adf5ed765086 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProvider.java @@ -18,24 +18,19 @@ */ package org.apache.pulsar.functions.auth; -import org.apache.commons.lang3.StringUtils; import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.client.impl.auth.AuthenticationToken; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import javax.naming.AuthenticationException; - import java.util.Optional; -import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.getToken; -import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; public class ClearTextFunctionTokenAuthProvider implements FunctionAuthProvider { @Override public void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData) { authConfig.setClientAuthenticationPlugin(AuthenticationToken.class.getName()); - authConfig.setClientAuthenticationParameters("token://" + functionAuthData.getData()); + authConfig.setClientAuthenticationParameters("token:" + new String(functionAuthData.getData())); } @Override diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index 596f23d65814d..de59fce46f1e3 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -32,24 +32,19 @@ import io.kubernetes.client.models.V1VolumeMount; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.pulsar.broker.authentication.AuthenticationDataSource; import org.apache.pulsar.client.impl.auth.AuthenticationToken; import org.apache.pulsar.functions.instance.AuthenticationConfig; -import org.apache.pulsar.functions.runtime.RuntimeUtils; import org.apache.pulsar.functions.utils.Actions; import org.apache.pulsar.functions.utils.FunctionDetailsUtils; -import javax.naming.AuthenticationException; import java.util.Collections; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.HTTP_HEADER_VALUE_PREFIX; import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.getToken; -import static org.apache.pulsar.client.impl.auth.AuthenticationDataToken.HTTP_HEADER_NAME; @Slf4j public class KubernetesSecretsTokenAuthProvider implements KubernetesFunctionAuthProvider { @@ -99,7 +94,7 @@ public void configureAuthenticationConfig(AuthenticationConfig authConfig, Funct @Override public void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAccount, FunctionAuthData functionAuthData) { - serviceAccount.addSecretsItem(new V1ObjectReference().name("pf-secret-" + functionAuthData.getData()).namespace(kubeNamespace)); + serviceAccount.addSecretsItem(new V1ObjectReference().name("pf-secret-" + new String(functionAuthData.getData())).namespace(kubeNamespace)); } @Override From 91aff8085a5f2a74cb42acad85a58a7d812c2e30 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Thu, 14 Mar 2019 18:27:24 -0700 Subject: [PATCH 18/22] add tests --- ...learTextFunctionTokenAuthProviderTest.java | 63 +++++++++ ...ubernetesSecretsTokenAuthProviderTest.java | 123 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProviderTest.java create mode 100644 pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProviderTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProviderTest.java new file mode 100644 index 0000000000000..3dde48b3f8167 --- /dev/null +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/ClearTextFunctionTokenAuthProviderTest.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.client.impl.auth.AuthenticationToken; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Optional; + +public class ClearTextFunctionTokenAuthProviderTest { + + @Test + public void testClearTextAuth() throws Exception { + + ClearTextFunctionTokenAuthProvider clearTextFunctionTokenAuthProvider = new ClearTextFunctionTokenAuthProvider(); + + Optional functionAuthData = clearTextFunctionTokenAuthProvider.cacheAuthData("test-tenant", + "test-ns", "test-func", new AuthenticationDataSource() { + @Override + public boolean hasDataFromCommand() { + return true; + } + + @Override + public String getCommandData() { + return "test-token"; + } + }); + + Assert.assertTrue(functionAuthData.isPresent()); + Assert.assertEquals(functionAuthData.get().getData(), "test-token".getBytes()); + + AuthenticationConfig authenticationConfig = AuthenticationConfig.builder().build(); + clearTextFunctionTokenAuthProvider.configureAuthenticationConfig(authenticationConfig, functionAuthData.get()); + + Assert.assertEquals(authenticationConfig.getClientAuthenticationPlugin(), AuthenticationToken.class.getName()); + Assert.assertEquals(authenticationConfig.getClientAuthenticationParameters(), "token:test-token"); + + + AuthenticationToken authenticationToken = new AuthenticationToken(); + authenticationToken.configure(authenticationConfig.getClientAuthenticationParameters()); + Assert.assertEquals(authenticationToken.getAuthData().getCommandData(), "test-token"); + } +} diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java new file mode 100644 index 0000000000000..14bc3e64e1bc1 --- /dev/null +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java @@ -0,0 +1,123 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pulsar.functions.auth; + +import io.kubernetes.client.ApiException; +import io.kubernetes.client.apis.CoreV1Api; +import io.kubernetes.client.models.V1Container; +import io.kubernetes.client.models.V1PodSpec; +import io.kubernetes.client.models.V1PodTemplateSpec; +import io.kubernetes.client.models.V1Secret; +import io.kubernetes.client.models.V1ServiceAccount; +import io.kubernetes.client.models.V1StatefulSet; +import io.kubernetes.client.models.V1StatefulSetSpec; +import org.apache.commons.lang.StringUtils; +import org.apache.pulsar.broker.authentication.AuthenticationDataSource; +import org.apache.pulsar.client.impl.auth.AuthenticationToken; +import org.apache.pulsar.functions.instance.AuthenticationConfig; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.Optional; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class KubernetesSecretsTokenAuthProviderTest { + + @Test + public void testConfigureAuthDataStatefulSet() { + + CoreV1Api coreV1Api = mock(CoreV1Api.class); + KubernetesSecretsTokenAuthProvider kubernetesSecretsTokenAuthProvider = new KubernetesSecretsTokenAuthProvider(coreV1Api, "default"); + + + V1StatefulSet statefulSet = new V1StatefulSet(); + statefulSet.setSpec( + new V1StatefulSetSpec().template( + new V1PodTemplateSpec().spec( + new V1PodSpec().containers( + Collections.singletonList(new V1Container()))))); + FunctionAuthData functionAuthData = FunctionAuthData.builder().data("foo".getBytes()).build(); + kubernetesSecretsTokenAuthProvider.configureAuthDataStatefulSet(statefulSet, functionAuthData); + + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getVolumes().size(), 1); + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getVolumes().get(0).getName(), "function-auth"); + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getVolumes().get(0).getSecret().getSecretName(), "pf-secret-foo"); + + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getContainers().size(), 1); + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().size(), 1); + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().get(0).getName(), "function-auth"); + Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().get(0).getMountPath(), "/etc/auth"); + } + + @Test + public void testConfigureAuthDataServiceAccount() { + + CoreV1Api coreV1Api = mock(CoreV1Api.class); + KubernetesSecretsTokenAuthProvider kubernetesSecretsTokenAuthProvider = new KubernetesSecretsTokenAuthProvider(coreV1Api, "default"); + + FunctionAuthData functionAuthData = FunctionAuthData.builder().data("foo".getBytes()).build(); + V1ServiceAccount serviceAccount = new V1ServiceAccount(); + + kubernetesSecretsTokenAuthProvider.configureAuthDataKubernetesServiceAccount(serviceAccount, functionAuthData); + + Assert.assertEquals(serviceAccount.getSecrets().size(), 1); + Assert.assertEquals(serviceAccount.getSecrets().get(0).getName(), "pf-secret-foo"); + Assert.assertEquals(serviceAccount.getSecrets().get(0).getNamespace(), "default"); + } + + @Test + public void testCacheAuthData() throws ApiException { + CoreV1Api coreV1Api = mock(CoreV1Api.class); + doReturn(new V1Secret()).when(coreV1Api).createNamespacedSecret(anyString(), any(), anyString()); + KubernetesSecretsTokenAuthProvider kubernetesSecretsTokenAuthProvider = new KubernetesSecretsTokenAuthProvider(coreV1Api, "default"); + Optional functionAuthData = kubernetesSecretsTokenAuthProvider.cacheAuthData("test-tenant", + "test-ns", "test-func", new AuthenticationDataSource() { + @Override + public boolean hasDataFromCommand() { + return true; + } + + @Override + public String getCommandData() { + return "test-token"; + } + }); + + Assert.assertTrue(functionAuthData.isPresent()); + Assert.assertTrue(StringUtils.isNotBlank(new String(functionAuthData.get().getData()))); + } + + @Test + public void configureAuthenticationConfig() { + CoreV1Api coreV1Api = mock(CoreV1Api.class); + KubernetesSecretsTokenAuthProvider kubernetesSecretsTokenAuthProvider = new KubernetesSecretsTokenAuthProvider(coreV1Api, "default"); + AuthenticationConfig authenticationConfig = AuthenticationConfig.builder().build(); + FunctionAuthData functionAuthData = FunctionAuthData.builder().data("foo".getBytes()).build(); + kubernetesSecretsTokenAuthProvider.configureAuthenticationConfig(authenticationConfig, functionAuthData); + + Assert.assertEquals(authenticationConfig.getClientAuthenticationPlugin(), AuthenticationToken.class.getName()); + Assert.assertEquals(authenticationConfig.getClientAuthenticationParameters(), "file:///etc/auth/token"); + } +} From 510a23f10e5d2f17d3a37d86c6e74443a90020b5 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Fri, 15 Mar 2019 14:24:25 -0700 Subject: [PATCH 19/22] remove service account creation --- .../auth/KubernetesFunctionAuthProvider.java | 7 - .../KubernetesSecretsTokenAuthProvider.java | 4 - .../functions/runtime/KubernetesRuntime.java | 147 ------------------ ...ubernetesSecretsTokenAuthProviderTest.java | 16 -- 4 files changed, 174 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java index 4ea229c942d0c..8b42c6417f20b 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesFunctionAuthProvider.java @@ -26,13 +26,6 @@ */ public interface KubernetesFunctionAuthProvider extends FunctionAuthProvider { - /** - * Configure service account spec generated for function based on function auth data - * @param serviceAccount service account spec for the function - * @param functionAuthData function auth data - */ - void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAccount, FunctionAuthData functionAuthData); - /** * Configure function statefulset spec based on function auth data * @param statefulSet statefulset spec for function diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index de59fce46f1e3..3047e038e3783 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -92,10 +92,6 @@ public void configureAuthenticationConfig(AuthenticationConfig authConfig, Funct authConfig.setClientAuthenticationParameters(String.format("file://%s/%s", DEFAULT_SECRET_MOUNT_DIR, FUNCTION_AUTH_TOKEN)); } - @Override - public void configureAuthDataKubernetesServiceAccount(V1ServiceAccount serviceAccount, FunctionAuthData functionAuthData) { - serviceAccount.addSecretsItem(new V1ObjectReference().name("pf-secret-" + new String(functionAuthData.getData())).namespace(kubeNamespace)); - } @Override public Optional cacheAuthData(String tenant, String namespace, String name, diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java index 523d649480b8d..fb51b36719642 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/KubernetesRuntime.java @@ -45,7 +45,6 @@ import io.kubernetes.client.models.V1PodTemplateSpec; import io.kubernetes.client.models.V1ResourceRequirements; import io.kubernetes.client.models.V1Service; -import io.kubernetes.client.models.V1ServiceAccount; import io.kubernetes.client.models.V1ServicePort; import io.kubernetes.client.models.V1ServiceSpec; import io.kubernetes.client.models.V1StatefulSet; @@ -231,9 +230,6 @@ public class KubernetesRuntime implements Runtime { public void start() throws Exception { try { - if (instanceConfig.getFunctionAuthenticationSpec() != null) { - createServiceAccount(); - } submitService(); submitStatefulSet(); @@ -260,66 +256,6 @@ public void start() throws Exception { } } - private String generateServiceAccount(InstanceConfig instanceConfig) { - return instanceConfig.getFunctionDetails().getTenant() - + "-" + instanceConfig.getFunctionDetails().getNamespace() - + "-" + instanceConfig.getFunctionDetails().getName() - + "-" + instanceConfig.getFunctionAuthenticationSpec().getData(); - } - - private void createServiceAccount() throws ApiException, InterruptedException { - - String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); - Function.FunctionAuthenticationSpec authenticationSpec = instanceConfig.getFunctionAuthenticationSpec(); - String serviceAccountName = generateServiceAccount(instanceConfig); - V1ServiceAccount serviceAccount = new V1ServiceAccount() - .metadata( - new V1ObjectMeta() - .name(serviceAccountName) - .namespace(jobNamespace)); - - // configure service account for auth data if necessary - functionAuthDataCacheProvider.configureAuthDataKubernetesServiceAccount(serviceAccount, getFunctionAuthData(authenticationSpec)); - - log.info("Creating service account with the following spec to k8 {} for function {}", appsClient.getApiClient().getJSON().serialize(serviceAccount), fqfn); - - Actions.Action createServiceAccount = Actions.Action.builder() - .actionName(String.format("Creating service account %s for function %s", serviceAccountName, fqfn)) - .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) - .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) - .supplier(() -> { - try { - coreClient.createNamespacedServiceAccount(jobNamespace, serviceAccount, "true"); - } catch (ApiException e) { - // already exists - if (e.getCode() == HTTP_CONFLICT) { - log.warn("Service account {} already present for function {}", serviceAccountName, fqfn); - return Actions.ActionResult.builder().success(true).build(); - } - - String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return Actions.ActionResult.builder() - .success(false) - .errorMsg(errorMsg) - .build(); - } - - return Actions.ActionResult.builder().success(true).build(); - }) - .build(); - - AtomicBoolean success = new AtomicBoolean(false); - Actions.newBuilder() - .addAction(createServiceAccount.toBuilder() - .onSuccess(ignore -> success.set(true)) - .build()) - .run(); - - if (!success.get()) { - throw new RuntimeException(String.format("Failed to create service account %s for function %s", serviceAccountName, fqfn)); - } - } - @Override public void join() throws Exception { // K8 functions never return @@ -330,7 +266,6 @@ public void join() throws Exception { public void stop() throws Exception { deleteStatefulSet(); deleteService(); - deleteServiceAccounts(); if (channel != null) { for (ManagedChannel cn : channel) { @@ -808,85 +743,6 @@ public void deleteService() throws InterruptedException { } } - private void deleteServiceAccounts() throws InterruptedException { - String fqfn = FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails()); - - String serviceAccountName = generateServiceAccount(instanceConfig); - Actions.Action deleteServiceAccount = Actions.Action.builder() - .actionName(String.format("Deleting service account %s for function %s", serviceAccountName, fqfn)) - .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) - .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) - .supplier(() -> { - try { - V1DeleteOptions v1DeleteOptions = new V1DeleteOptions(); - v1DeleteOptions.setGracePeriodSeconds(0L); - v1DeleteOptions.setPropagationPolicy("Foreground"); - - coreClient.deleteNamespacedServiceAccount(serviceAccountName, jobNamespace, - v1DeleteOptions, null, null, null, null); - } catch (ApiException e) { - // if already deleted - if (e.getCode() == HTTP_NOT_FOUND) { - return Actions.ActionResult.builder().success(true).build(); - } - - String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return Actions.ActionResult.builder() - .success(false) - .errorMsg(errorMsg) - .build(); - } - return Actions.ActionResult.builder().success(true).build(); - }) - .build(); - - Actions.Action waitForServiceAccountDeletion = Actions.Action.builder() - .actionName(String.format("Waiting for service account %s for function %s to complete deletion", serviceAccountName, fqfn)) - .numRetries(KubernetesRuntimeFactory.NUM_RETRIES) - .sleepBetweenInvocationsMs(KubernetesRuntimeFactory.SLEEP_BETWEEN_RETRIES_MS) - .supplier(() -> { - try { - coreClient.readNamespacedServiceAccount(serviceAccountName, jobNamespace, null, null, null); - - } catch (ApiException e) { - // statefulset is gone - if (e.getCode() == HTTP_NOT_FOUND) { - return Actions.ActionResult.builder().success(true).build(); - } - String errorMsg = e.getResponseBody() != null ? e.getResponseBody() : e.getMessage(); - return Actions.ActionResult.builder() - .success(false) - .errorMsg(errorMsg) - .build(); - } - return Actions.ActionResult.builder() - .success(false) - .build(); - }) - .build(); - - AtomicBoolean success = new AtomicBoolean(false); - Actions.newBuilder() - .addAction(deleteServiceAccount.toBuilder() - .continueOn(true) - .build()) - .addAction(waitForServiceAccountDeletion.toBuilder() - .continueOn(false) - .onSuccess(ignore -> success.set(true)) - .build()) - .addAction(deleteServiceAccount.toBuilder() - .continueOn(true) - .build()) - .addAction(waitForServiceAccountDeletion.toBuilder() - .onSuccess(ignore -> success.set(true)) - .build()) - .run(); - - if (!success.get()) { - throw new RuntimeException(String.format("Failed to delete service account %s for function %s", serviceAccountName, fqfn)); - } - } - protected List getExecutorCommand() { return Arrays.asList( "sh", @@ -1033,9 +889,6 @@ private V1PodSpec getPodSpec(List instanceCommand, Function.Resources re // Configure secrets secretsProviderConfigurator.configureKubernetesRuntimeSecretsProvider(podSpec, PULSARFUNCTIONS_CONTAINER_NAME, instanceConfig.getFunctionDetails()); - log.info("Setting service account {} for function {}", generateServiceAccount(instanceConfig), FunctionDetailsUtils.getFullyQualifiedName(instanceConfig.getFunctionDetails())); - podSpec.setServiceAccount(generateServiceAccount(instanceConfig)); - return podSpec; } diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java index 14bc3e64e1bc1..eac937f790e86 100644 --- a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProviderTest.java @@ -71,22 +71,6 @@ public void testConfigureAuthDataStatefulSet() { Assert.assertEquals(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().get(0).getMountPath(), "/etc/auth"); } - @Test - public void testConfigureAuthDataServiceAccount() { - - CoreV1Api coreV1Api = mock(CoreV1Api.class); - KubernetesSecretsTokenAuthProvider kubernetesSecretsTokenAuthProvider = new KubernetesSecretsTokenAuthProvider(coreV1Api, "default"); - - FunctionAuthData functionAuthData = FunctionAuthData.builder().data("foo".getBytes()).build(); - V1ServiceAccount serviceAccount = new V1ServiceAccount(); - - kubernetesSecretsTokenAuthProvider.configureAuthDataKubernetesServiceAccount(serviceAccount, functionAuthData); - - Assert.assertEquals(serviceAccount.getSecrets().size(), 1); - Assert.assertEquals(serviceAccount.getSecrets().get(0).getName(), "pf-secret-foo"); - Assert.assertEquals(serviceAccount.getSecrets().get(0).getNamespace(), "default"); - } - @Test public void testCacheAuthData() throws ApiException { CoreV1Api coreV1Api = mock(CoreV1Api.class); From 51f33ffa48b41719e34fff5f75dc9140cd23be86 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Sat, 16 Mar 2019 22:22:24 -0700 Subject: [PATCH 20/22] cleanup unused imports --- .../functions/auth/KubernetesSecretsTokenAuthProvider.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java index 3047e038e3783..ff2cd57afe82e 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/KubernetesSecretsTokenAuthProvider.java @@ -22,11 +22,9 @@ import io.kubernetes.client.apis.CoreV1Api; import io.kubernetes.client.models.V1DeleteOptions; import io.kubernetes.client.models.V1ObjectMeta; -import io.kubernetes.client.models.V1ObjectReference; import io.kubernetes.client.models.V1PodSpec; import io.kubernetes.client.models.V1Secret; import io.kubernetes.client.models.V1SecretVolumeSource; -import io.kubernetes.client.models.V1ServiceAccount; import io.kubernetes.client.models.V1StatefulSet; import io.kubernetes.client.models.V1Volume; import io.kubernetes.client.models.V1VolumeMount; From 29885af29ff665e9f5ed9b13e67c7c2ad151dff7 Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Mon, 18 Mar 2019 10:56:16 -0700 Subject: [PATCH 21/22] add field for auth provider --- pulsar-functions/proto/src/main/proto/Function.proto | 4 ++++ .../org/apache/pulsar/functions/auth/FunctionAuthData.java | 2 ++ .../pulsar/functions/worker/rest/api/ComponentImpl.java | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pulsar-functions/proto/src/main/proto/Function.proto b/pulsar-functions/proto/src/main/proto/Function.proto index 8843b74a84592..f2c4885fb8627 100644 --- a/pulsar-functions/proto/src/main/proto/Function.proto +++ b/pulsar-functions/proto/src/main/proto/Function.proto @@ -146,6 +146,10 @@ message FunctionMetaData { message FunctionAuthenticationSpec { bytes data = 1; + /** + * classname of the function auth provicer this data is relevant to + */ + string provider = 2; } message Instance { diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java index b41f03697823d..d88232b1bd2e4 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java @@ -28,4 +28,6 @@ */ public class FunctionAuthData { private byte[] data; + // classname of the function auth provicer this data is relevant to + private String provider; } diff --git a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java index 0a16cd5169d8c..23f479a565d8c 100644 --- a/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java +++ b/pulsar-functions/worker/src/main/java/org/apache/pulsar/functions/worker/rest/api/ComponentImpl.java @@ -396,7 +396,8 @@ public void registerFunction(final String tenant, if (functionAuthData.isPresent()) { functionMetaDataBuilder.setFunctionAuthSpec( Function.FunctionAuthenticationSpec.newBuilder() - .setData(ByteString.copyFrom(functionAuthData.get().getData())).build()); + .setData(ByteString.copyFrom(functionAuthData.get().getData())) + .build()); } } catch (Exception e) { log.error("Error caching authentication data for {} {}/{}/{}", componentType, tenant, namespace, componentName, e); From 3a4e66fe4631a93b4c5b0f4f539737589c996d1a Mon Sep 17 00:00:00 2001 From: Jerry Peng Date: Mon, 18 Mar 2019 12:45:17 -0700 Subject: [PATCH 22/22] adding comments --- pulsar-functions/proto/src/main/proto/Function.proto | 6 ++++++ .../apache/pulsar/functions/auth/FunctionAuthData.java | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pulsar-functions/proto/src/main/proto/Function.proto b/pulsar-functions/proto/src/main/proto/Function.proto index f2c4885fb8627..fba37e40a615a 100644 --- a/pulsar-functions/proto/src/main/proto/Function.proto +++ b/pulsar-functions/proto/src/main/proto/Function.proto @@ -145,6 +145,12 @@ message FunctionMetaData { } message FunctionAuthenticationSpec { + /** + * function authentication related data that the function authentication provider + * needs to cache/distribute to all workers support function authentication. + * Depending on the function authentication provider implementation, this can be the actual auth credentials + * or a pointer to the auth credentials that this function should use + */ bytes data = 1; /** * classname of the function auth provicer this data is relevant to diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java index d88232b1bd2e4..be176689fc56c 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/auth/FunctionAuthData.java @@ -27,7 +27,15 @@ * A wrapper for authentication data for functions */ public class FunctionAuthData { + /** + * function authentication related data that the function authentication provider + * needs to cache/distribute to all workers support function authentication. + * Depending on the function authentication provider implementation, this can be the actual auth credentials + * or a pointer to the auth credentials that this function should use + */ private byte[] data; - // classname of the function auth provicer this data is relevant to + /** + * classname of the function auth provicer this data is relevant to + */ private String provider; }