Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing authentication for Pulsar Functions #3735

Merged
merged 24 commits into from Mar 19, 2019

Conversation

jerrypeng
Copy link
Contributor

@jerrypeng jerrypeng commented Mar 2, 2019

Motivation

Currently authentication for functions can only work with static credentials which makes every function have the same credentials as any other function which is not practical. Since functions should have the same credentials as the users that submit them.

This is an initial implementation that solves the issue

Currently, users will not be able to specify which function auth provider to use, but I can add that in a subsequent PR once everyone is onboard with this

Master issue: #3763

The workflow of the current API is as such:

  1. User submits function worker with auth data
  2. Worker caches information based on auth data ( implementation specific) that gets stored in the function meta topic which gets distributed to every worker
  3. A worker that is assigned to run an instance of the function gets the auth info from the function metadata topic and setups/configures the function instance to support auth (Details are implementation specific)

Below is a diagram that demonstrates the end-to-end process of function authentication for a possible implementation on function authentication in Kubernetes using tokens issued by Hashicorp's Vault:

Screen Shot 2019-03-14 at 12 07 37 PM

The KubernetesFunctionAuthenticationProvider implementation is based on the above diagram

@jerrypeng
Copy link
Contributor Author

rerun java8 tests
rerun integration tests
rerun cpp tests

Copy link
Member

@sijie sijie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jiazhai since you are working on kerberos authentication, it would be good for you to take a look at the interface here, to make sure if people want to implement kerberos authentication using this interface, is it straightforward to implement one?

}

message FunctionAuthenticationSpec {
string data = 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering why are you using string, not bytes. my concern of string is that encoding with locales make things very tricky and hard to debug.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know what kind of params/data would need to be stored so I just based this off of the authParams String format we already have in Pulsar. We can change this to bytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also you can always base64 encode a binary to be stored as a string

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jerrypeng bytes is a more natural representation :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sijie i have changed

import org.apache.pulsar.functions.instance.AuthenticationConfig;
import org.apache.pulsar.functions.proto.Function;

public interface FunctionAuthProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please add javadoc comments in the interface? otherwise other people will have difficulties on understanding how to implement an Auth provider.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure


void configureAuthenticationConfig(AuthenticationConfig authConfig, Function.FunctionAuthenticationSpec functionAuthenticationSpec);

Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid exposing protobuf structures in the interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup we can have a wrapper for this

import org.apache.pulsar.functions.instance.AuthenticationConfig;
import org.apache.pulsar.functions.proto.Function;

public interface FunctionAuthProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a very high level question here - why do you need a separate AuthProvider? why can you use the AuthenticationProvider interface at the broker? what are the real differences between these two interfaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the AuthenticationProvider is interface to simply do authentication. The interface here for functions is a little bit more involved. It needs to have to ability to:

  1. We need to be able to cache some data in the function meta data topic. This could be the auth data itself or a pointer the auth data.
  2. Based on the auth data of the function, we need to be able to manipulate the function runtime somehow (depends on runtime)

This interface doesn't actually authenticate a user but facilitates the authentication process for functions. Whether the name of the interface is appropriate can be up for discussion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. A better name or a javadoc for this class is good to have for this class.

public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name,
AuthenticationDataSource authenticationDataSource)
throws Exception {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need return a null here? or shall returning a spec with a data?

Copy link
Contributor Author

@jerrypeng jerrypeng Mar 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow. We can return whatever we want there as long as it doesn't trigger anything

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean returning null usually results in NPE if you don't take care of it at the callers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an API perspective, it's better to use Optional<X> whenever the returned object might not be applicable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok we can change this method to return Optional<Function.FunctionAuthenticationSpe>. Is that what you would recommend?

try {
setupClient();
} catch (Exception e) {
throw new RuntimeException(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a logging statement before throwing a RuntimeException?
RuntimeException is very hard to debug.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add

@Override
public void close() {
// cancel liveness checker before terminating runtime.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure the change code here is related to Authentication. If there are multiple changes coupled in this pull request, can we try to separate them into multiple different pull requests in future? or at least highlight them at the description?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a change that was required in a previous version of the implementation. I don't think its required anymore

try {
runtime.terminate();
} catch (Exception e) {
log.warn("Failed to terminate function runtime: {}", e, e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are logging two e.

String fqfn = FunctionDetailsUtils.getFullyQualifiedName(details);
log.info("{}-{} Terminating function...", fqfn,functionRuntimeInfo.getFunctionInstance().getInstanceId());

if (functionRuntimeInfo.getRuntimeSpawner() != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that the logic here are not related.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here is relevant since in terminate we have to call cleanUpAuthData before we set the runtimespawner to be null

.run();

if (!success.get()) {
throw new RuntimeException(String.format("Failed to create service account %s for function %s", serviceAccountName, fqfn));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging before throwing runtime exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be logged else where

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be logged else where

sorry, at where? if you don't catch RuntimeException, isn't the context lost?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivankelly ivankelly self-requested a review March 4, 2019 20:37
@jerrypeng
Copy link
Contributor Author

rerun java8 tests
rerun integration tests

@jerrypeng
Copy link
Contributor Author

rerun java8 tests
rerun cpp tests

@jerrypeng
Copy link
Contributor Author

@jiazhai can you review this please? Thanks.

@jerrypeng
Copy link
Contributor Author

rerun java8 tests
rerun integration tests

@jerrypeng
Copy link
Contributor Author

rerun java8 tests

@jerrypeng jerrypeng requested a review from sijie March 7, 2019 05:29
@jerrypeng
Copy link
Contributor Author

@merlimat @sijie @srkukarni @jiazhai please review. Thanks!

Copy link
Contributor

@ivankelly ivankelly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some unit testing. I know it's hard for k8s, but at least for plaintext, it would be good to seem something exercising the interfaces.

The change seems to assume token auth in a couple of places. This should be explicitly checked, as in, it shouldn't even try to distribute the auth data if anything other than token is used for now.

@@ -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 ";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than making this public, you should make getToken(AuthenticationDataSource) static and call it from the other locations.

}

message FunctionAuthenticationSpec {
bytes data = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also include method, which the function runner can use to figure out which function auth provider to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function auth provider should be a cluster wide setting and not on a per function basis right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comment to clarify the content and usage of "data" field, to summarize the below discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@merlimat i have add comments

/**
* Kubernetes runtime specific functions authentication provider
*/
public interface KubernetesFunctionAuthProvider extends FunctionAuthProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bad sign if for the first serious use of an abstraction you need to create another abstraction to work work around shortcomings in the first abstraction. However, I don't think it's even needed in this case.

The functionAuthData returned by cacheAuthData should contain the actual token. The caller of cacheAuthData can then create a secret, and then mount that secret on each of the instance pods. When the instance runs, it can always check for this secret and if it is mounted, the data can be read in and passed to configureAuthenticationConfig.

I don't see why service accounts are needed at all here.

Copy link
Contributor Author

@jerrypeng jerrypeng Mar 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bad sign if for the first serious use of an abstraction you need to create another abstraction to work work around shortcomings in the first abstraction. However, I don't think it's even needed in this case.

Why is that bad? I designed this so that it is cleaner compared to AuthenticationDataSource where you have a mixture of interfaces to support different authentication methods. I imagine in the future, different runtimes will have different requirements / interfaces needed to support authentication. It doesn't make sense to clutter them all together.

The functionAuthData returned by cacheAuthData should contain the actual token.

Why should functionAuthData contain the actual token? This is implementation specific data. Different implementations of FunctionAuthenticationProvider should have the flexibility to use it in a way that makes sense for the implementation.

The caller of cacheAuthData can then create a secret, and then mount that secret on each of the instance pods.

I think there is some misunderstanding here. The architecture of Pulsar Function decouples submitting functions and running functions. The worker that the user actually submits the function do is not necessarily going to be the same worker that is going to run the function. However, the auth data will only be passed to the worker that the user at first submits his or her function to. That is why that worker needs to be able to distribute that auth data or data based on that auth data to the rest of the workers that will potentially need to run a function instance. The interface cacheAuthData returns the data that needs to be distributed to the other workers via the function metadata topic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is some misunderstanding here.

Ya, I needed to map out the interactions. See my top level comment.

@jerrypeng
Copy link
Contributor Author

@ivankelly I don't think I am assuming a token based authentication. The current implementation of the interfaces only accept token based authentication. Do you have a specific location you can point me to that only will work with token based auth?

Copy link
Contributor

@merlimat merlimat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of comments on the new interfaces.

Also, can you add a description of the workflow for the management of credentials when the functions are submitted or scheduled?

}

message FunctionAuthenticationSpec {
bytes data = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does data includes the credential? Or a pointer to the credentials? Can you add comment to clarify the field meaning?
If it includes credentials, should this be included in the function metadata?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it can include the actual credentials (not secure) or a pointer to the credentials. This all depends on the underlying implementation of the interface. I left this field open ended so that there can be flexibility of what can be stored in here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should always include the credentials. If you want to make it secure, make it secure using the caller of cacheAuthData, not within cacheAuthData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivankelly I don't quite follow what you are proposing. This protobuf message will be stored in the function metadata topic. This message was designed with a generic purpose to store some data for the function authentication provider so that it can distribute it to all the workers. What that data is is up to the function authentication provider implementation. This gives us flexibility in the types of authentication we can support in the future.

pulsar-functions/runtime/pom.xml Show resolved Hide resolved
* @return
* @throws Exception
*/
FunctionAuthData cacheAuthData(String tenant, String namespace, String name, AuthenticationDataSource authenticationDataSource) throws Exception;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of caching auth data? If there are credentials being submitted, we should immediately store them in the secrets backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mechanism to distribute auth credentials or pointers to auth credentials based on implementtion to workers that need to run an instance of the function. Since in the function architecture, submitting the function and running the function are decoupled and might not happen on the same machine we need a mechanism to distribute some information to workers about how to configure authentication for individual function instances

public Function.FunctionAuthenticationSpec cacheAuthData(String tenant, String namespace, String name,
AuthenticationDataSource authenticationDataSource)
throws Exception {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an API perspective, it's better to use Optional<X> whenever the returned object might not be applicable.

@merlimat merlimat added this to the 2.4.0 milestone Mar 14, 2019
@jerrypeng
Copy link
Contributor Author

@ivankelly I have also added some unit tests

Copy link
Member

@sijie sijie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me.

I have only one comment around functions depending on broker module, this might potentially create some weird recurring dependency when function worker is running as part of broker in future.

I would suggest @jiazhai taking a closer look at the interface since he is working on kerberos authentication to make sure it is easy to add kerberos to functions in future.


import java.util.Optional;

import static org.apache.pulsar.broker.authentication.AuthenticationProviderToken.getToken;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure it is a good idea to reference a method in broker module. this would produce potentially very bad dependency tree. If it is common enough, it should be in pulsar-common, otherwise I would suggest just redoing the logic in functions module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya @merlimat and I discussed this as well. We could just move the get token logic to some utils class in pulsar-common. Originally I actually was just redoing the logic in the functions module, but @ivankelly suggested to reuse the code in the AuthenticationProviderToken class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, break it into common

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivankelly its kind of messy extracting the getToken method from AuthenticationProviderToken since it relies on the on many package local String declared in the class.

I think the cleanest way is just duplicate some of the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be a big move since AuthenticationDataSource would also need to move the pulsar-common and all the things that AuthenticationDataSource depends on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sijie I think pulsar-function-runtime is going to have to depend on pulsar-broker-common for the time being since AuthenticationDataSource interface in part of pulsar-broker-common and pulsar-function-worker already depends on pulsar-broker-common anyways

@jerrypeng
Copy link
Contributor Author

rerun java8 tests

@ivankelly
Copy link
Contributor

@jerrypeng So, in the sequence you have, the service account bit is entirely unnecessary. Rather than specifying a service account for the StatefulSet(?), you can just attach the secret as a volumeMount or as a env variable.

@ivankelly
Copy link
Contributor

@jerrypeng

So mapping out the interactions here, yields the following sequence diagram.
image

I think this can be similifed a fair bit.

First of all, service accounts are not needed, you can attached the secret directly to the stateful set. In fact, thats exactly what you are doing. service accounts are for pods to authenticate with k8s services. We're not doing that, and we don't need to do that for this implementation. It may be needed for vault at a later stage, but let's not make assumptions about how that'll be used until we have concretely worked out the flow for that.

Secondly, i don't think we need an interface for attaching the secret to the stateful set. Whatever the auth data is that we are passing in, we should assume it is secret, so if there is auth data, the k8s runtime should attach that as a secret. Then it is up to configureAuthenticationConfig to know how to do something with that authData. So we should move the attachment of the secret volume and the mount into the k8s runtime itself.

so the resulting sequence would look like
image


// adjust the auth config to support auth
if (instanceConfig.getFunctionAuthenticationSpec() != null) {
getAuthProvider().configureAuthenticationConfig(authConfig, getFunctionAuthData(instanceConfig.getFunctionAuthenticationSpec()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this call is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is to configure the authConfig appropriately to support auth because it will get passed into KubernetesRuntime and eventually in the the function instance. And the fields in the authConfig is what the function instance or the pulsar client in the instance will use for authentication

* @param authConfig authentication configs passed to the function instance
* @param functionAuthData function authentication data that is provider specific
*/
void configureAuthenticationConfig(AuthenticationConfig authConfig, FunctionAuthData functionAuthData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configureAuthenticationConfig sounds weird, why not just configureAuth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure I can do that but the reason I named the method configureAuthenticationConfig is because we are literally configuring AuthenticationConfig based on FunctionAuthData

* A wrapper for authentication data for functions
*/
public class FunctionAuthData {
private byte[] data;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a string member here to hold which functions auth provider created the auth data. this can be used by the k8s runtime provider at least validate the auth data is of the type expected. I know this will be configured at a cluster level, but people screw up configuration all the time. Better to be defensive.

@jerrypeng
Copy link
Contributor Author

@ivankelly responding to your comments

First of all, service accounts are not needed, you can attached the secret directly to the stateful set. In fact, thats exactly what you are doing. service accounts are for pods to authenticate with k8s services. We're not doing that, and we don't need to do that for this implementation. It may be needed for vault at a later stage, but let's not make assumptions about how that'll be used until we have concretely worked out the flow for that.

let me experiment with not having a service account and see if that works

Secondly, i don't think we need an interface for attaching the secret to the stateful set. Whatever the auth data is that we are passing in, we should assume it is secret, so if there is auth data, the k8s runtime should attach that as a secret. Then it is up to configureAuthenticationConfig to know how to do something with that authData. So we should move the attachment of the secret volume and the mount into the k8s runtime itself.

I don't believe that is the correct design choice because now the runtime is making assumptions on what the data in FunctionAuthData is. The current implementation KubernetesSecretsTokenAuthProvider stores the name of the secret (pointer to it) in the data field of FunctionAuthData. That is an workflow establish in KubernetesSecretsTokenAuthProvider because we cache the name via cacheAuthData in the function metadata topic. How can we make that assumption for another FunctionAuthProvider impl? I could have implemented another version of a Kubernetes Function Authentication Provider that puts the actual token in the data field of FunctionAuthData and set it explicitly as a env variable in the pod spec. While that approach might not be secure, I would like implementations to have that flexibility and not to pigeon hole implementation to a certain workflow.

@ivankelly
Copy link
Contributor

@jerrypeng

I don't believe that is the correct design choice because now the runtime is making assumptions on what the data in FunctionAuthData is.

What you are doing now in the case of KubernetesFunctionAuthProvider is premature generalization. We only have one use case for it, and that's very clear in what methods are in the interface. Something for vault with dynamic tokens (i think i figured out the workload for this btw) would need something very different.

Generalization for this interface doesn't make sense even. The AuthProvider interface is a way for the runtime to allow common rest code code to pass information to the common instance code. But between these two points, everything is k8s specific. So we should treat it as such.

The current implementation KubernetesSecretsTokenAuthProvider stores the name of the secret (pointer to it) in the data field of FunctionAuthData. That is an workflow establish in KubernetesSecretsTokenAuthProvider because we cache the name via cacheAuthData in the function metadata topic. How can we make that assumption for another FunctionAuthProvider impl?

We can make this assumption for all k8s based auth providers, because secrets is the way to pass around sensitive information in k8s.

@jerrypeng
Copy link
Contributor Author

@ivankelly you are correct service accounts are not needed to mount secrets in pods. I have remove the service account related code

@jerrypeng
Copy link
Contributor Author

rerun java8 tests

Copy link
Contributor

@merlimat merlimat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

message FunctionAuthenticationSpec {
bytes data = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comment to clarify the content and usage of "data" field, to summarize the below discussion.

"--path",
bkPath,
"--destination-file",
userCodeFilePath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not for this PR) instead of passing all these as CLI options, we should instead place these into client.conf which is already automatically picked up by pulsar-admin (and potentially we can reuse also for functions).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@jerrypeng
Copy link
Contributor Author

rerun java8 tests

@jerrypeng
Copy link
Contributor Author

rerun integration tests

@merlimat
Copy link
Contributor

run integration tests

@merlimat merlimat merged commit b50760c into apache:master Mar 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants