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

Refactor Istio code before adding Mixer precondition checks (#1403) #1650

Merged
merged 13 commits into from
Oct 12, 2017

Conversation

pcalcado
Copy link
Contributor

@pcalcado pcalcado commented Sep 20, 2017

This consolidates some of the Istio-specific logic previously duplicated across the H2 and HTTP modules and increases test coverage in preparation for #1606

Reviewers will likely be interested in:

Copy link
Member

@adleong adleong left a comment

Choose a reason for hiding this comment

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

Sorry that it took so long for me to get to this review. I hope my comments make sense. I'm happy to chat more about this.

def get(domain: String): Future[Option[Cluster]]
}

class ClusterCacheBackedByApi(client: DiscoveryClient) extends ClusterCache {
Copy link
Member

Choose a reason for hiding this comment

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

This feels a bit verbose. How do you feel about something like ApiClusterCache?

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 tend to prefer more descriptive names, but I am happy with the suggestion. The only thing I'd suggest is that we keep the same naming pattern around things, do we usually go with Qualifier + Thing, as in Api ClusterCache or do we go the other way around? Looking around the code, it seems like so far we have relied on packages to namespace different implementations but keep the same name, should I do this here?

Copy link
Member

Choose a reason for hiding this comment

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

third option: ClusterCache with Api (Alex will hate this :D)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:P I actually find the cake pattern super cute, but had way too many problems debugging badly wired software 😤

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a super strong opinion about naming. I have a mild preference for a more concise name. Using package namespace to differentiate would also be fine.

import io.buoyant.k8s.ClientConfig
import io.buoyant.k8s.istio.mixer.MixerClient

trait IstioConfigurator {
Copy link
Member

Choose a reason for hiding this comment

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

As far as I can tell, nothing there depends on the current object's state. Could these methods just be static method instead? (ie turn this trait into an object)

I tend to shy away from the mixin pattern when possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm.. that's a good point. I think this had some template pattern logic at first but was gradually removed. Let me explore it, brb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

* are common in both types of objects.
*
*/
trait IstioDataFlow {
Copy link
Member

Choose a reason for hiding this comment

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

All of the information this trait provides is derived from the DstBoundCtx and not from any underlying request or response object. So it seems a bit misleading to attach this to IstioRequest and IstioResponse objects. It is also confusing that the vals on this trait are computed from the DstBoundCtx and saved at constructor time. This means that the exact position in the stack where this object is constructed will determine this values, which is pretty non-intuitive.

I would try to keep IstioDataFlow separate from the Request/Response objects and try to make it clear that constructing an IstioDataFlow takes a snapshot of the current DstBoundCtx.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are absolutely right, and I had felt some of this already. Unfortunately this branch didn't pick the changes, I've added them: https://github.com/linkerd/linkerd/pull/1650/files#diff-042aed18959e3a7caabf44f4294034ef

I am still not happy with CurrentIstioPath, but I ran out of ideas here.

duration
response.responseCode,
request.requestedPath,
response.targetService,
Copy link
Member

Choose a reason for hiding this comment

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

Values from the IstioDataFlow trait are derived from the local context so response.targetService could just as easily be request.targetService or even new IstioDataFlow {}.targetService

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, fixed with the same merge as the one above


import com.twitter.util.Duration

case class IstioResponse(statusCode: Int, duration: Duration) extends IstioDataFlow {
Copy link
Member

Choose a reason for hiding this comment

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

Any particular reason why IstioRequest is parameterized on the underlying request type and contains the underlying request but IstioResponse is not parameterized and does not include the underlying response?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed on the same as above

val routeRules: Activity[Map[String, RouteRule]]
}

class RouteCacheBackedByApi(api: ApiserverClient) extends RouteCache {
Copy link
Member

Choose a reason for hiding this comment

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

Name feels a bit verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Waiting for resolution on similar comment

import com.twitter.util.Duration
import io.buoyant.k8s.istio._
import istio.mixer.v1._

Copy link
Member

Choose a reason for hiding this comment

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

rm blank line between imports

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Interestingly enough, this seems to be a default on IntelliJ. Maybe we should upload a common IntelliJ formatting configuration file?

@@ -6,7 +6,7 @@ import com.twitter.finagle.http.{Request, Response}
import com.twitter.util.Future
import io.buoyant.k8s.istio.ApiserverClient.RouteRuleConfig
import io.buoyant.test.{Awaits, Exceptions}
import istio.proxy.v1.config.{DestinationWeight, RouteRule}
import _root_.istio.proxy.v1.config.{DestinationWeight, RouteRule}
Copy link
Member

Choose a reason for hiding this comment

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

what is the root prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Intellij being silly with protobuf imports -_- fixed

val istioIdentifier = new InternalTrafficIdentifier[Request](pfx, baseDtab, routeCache, clusterCache, mixerClient, new H2IstioRequestHandler)

override def apply(originalRequest: Request): Future[RequestIdentification[Request]] = {
val req = H2IstioRequest(originalRequest)
Copy link
Member

Choose a reason for hiding this comment

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

At this point the DstBoundCtx context has not been populated so attempting to call any of the methods from IstioDataFlow on this IstioRequest will fail.

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, this was fixed when we moved it to where Logger was. Current Identifier has no DstBoundCtx: https://github.com/linkerd/linkerd/pull/1650/files#diff-64056934f2ceb2827c6a08c5026e0ddeR20

import ClusterCache._
case class Cluster(dest: String, port: String)

trait ClusterCache extends Closable {
Copy link
Member

Choose a reason for hiding this comment

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

I assume this is factored this way for test-ability.

Instantiating a ClusterCacheBackedByApi with a test client that returns mock data would test more of the codepath than implementing a test ClusterCache. But it would also be a lot less ergonomic. I just wanted to bring it up so we can consider the tradeoff.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'm not really a big fan of the trait X/class XBackedByApi pattern that shows up a couple times in this PR. Elsewhere in Linkerd, when we need to mock an external API for testing, I think the general pattern is to inject a mock API with overrides, like this. Like Alex said, this would exercise more of the implementation, and I think it also results in a less complex non-test codebase?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting, the idea behind the use of an interface was to increase ergonomics. My thinking here is that the usual pattern of implementing a fake Service[Req,Resp] on tests have a few drawbacks:

  1. I think that it leaks a lot of "infrastructure" types into higher level tests. These tests shouldn't have any reason to even know about Finagle or any other piece of infrastructure
  2. Maybe more important, I also think that it also creates coupling between the consumer of a type and its implementation. For example, consider https://github.com/linkerd/linkerd/pull/1650/files#diff-bcd0445a689106c21b39b7304f665801R36 . If we follow the usual pattern of implementing a fake Service[Req,Resp] there then the test for InternalTrafficIdentifier would make assumptions about the API used by ClusterCacheBackedByApi. If the next version of the API used by this class changes the URL or any other implementation detail we would have to change not only the code and tests for ClusterCacheBackedByApi but also the tests for InternalTrafficIdentifier. This feels like accidental coupling.
  3. I am not sure if I agree that this would exercise the codebase more, at least in any useful manner. One rule of thumb that I like to follow when it comes to testing is "don't mock what you don't own", and in this case I think that we might be trying to use unit tests for what should be integration tests. Instead, I would suggest that we keep as much as possible of our own logic independent from these dependencies (which drives back to the 1st point) and use integration tests for those instead of mocking APIs that aren't under our control.
  4. The concept of ergonomics or complex code that we are discussing here are subjective, but I have the impression that one of the reasons for what I perceive to be low test coverage around these boundary classes is because it is actually a lot of work to have those fake Service[Req,Resp], so much so that we tend to have a single "fixture" reused across multiple tests, which requires tests to use special requests to know which part of the fixture will be hit.

But with all that said, Linkerd is an existing code base and I am happy to comply with the existing testing strategy if we collectively think that's the way to go.

Copy link
Member

Choose a reason for hiding this comment

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

I have a lot of opinions about testing that we can discuss at great length sometime in the future. But I think this is fine.

@hawkw hawkw added this to the 1.3.0 milestone Sep 22, 2017
Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

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

I agree with most of @adleong's comments, plus some minor style nits that you're free to ignore.

import ClusterCache._
case class Cluster(dest: String, port: String)

trait ClusterCache extends Closable {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'm not really a big fan of the trait X/class XBackedByApi pattern that shows up a couple times in this PR. Elsewhere in Linkerd, when we need to mock an external API for testing, I think the general pattern is to inject a mock API with overrides, like this. Like Alex said, this would exercise more of the implementation, and I think it also results in a less complex non-test codebase?


case class SourceDomainIstioAttribute(val value: String) extends IstioAttribute[String]("source.domain", StringAttribute, "The domain suffix part of the source service, excluding the name and the namespace. svc.cluster.local")

case class SourceUidIstioAttribute(val value: String) extends IstioAttribute[String]("source.uid", StringAttribute, "Platform-specific unique identifier for the client instance of the source service. kubernetes://redis-master-2353460263-1ecey.my-namespace")
Copy link
Member

Choose a reason for hiding this comment

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

this might be overly nitpicky, but can we insert some line breaks here? I think lines this long are hard to read especially when my editor wraps them weirdly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you let me know what you think of: https://github.com/linkerd/linkerd/pull/1650/files#diff-0a76be1349b81581e237715901125ebcR35 ?

I don't have a particular preference—I usually go with the Editor's default.n Like I mentioned before, it would probably be a good idea if we all shared a single formatter configuration file.

Copy link
Member

Choose a reason for hiding this comment

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

I like that alignment style for parameters, but most of the rest of the Linkerd codebase puts parameter lists on the next line, one tab level in, like

class Foo(
  a: A,
  b: B
) extends Bar {
...
}

rather than

class Foo(a: A,
          b: B) 
  extends Bar {
...
}

Copy link
Member

Choose a reason for hiding this comment

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

I agree that we definitely ought to have a shared config file for formatting. Do note that, while we don't have one you can plug into IDEA at the moment, running sbt test will also run Scalariform on everything.


def responseCode: ResponseCodeIstioAttribute = ResponseCodeIstioAttribute(statusCode)

def responseDuration: ResponseDurationIstioAttribute = ResponseDurationIstioAttribute(duration)
Copy link
Member

Choose a reason for hiding this comment

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

once again, I'd really like to see some more line breaks in this file. I'd find something like

package io.buoyant.k8s.istio

import com.twitter.util.Duration

case class IstioResponse[Resp](
  statusCode: Int,
  duration: Duration,
  resp: Option[Resp]
) extends IstioAttributesHolder {

  def responseCode: ResponseCodeIstioAttribute = 
    ResponseCodeIstioAttribute(statusCode)

  def responseDuration: ResponseDurationIstioAttribute = 
    ResponseDurationIstioAttribute(duration)

}

much more readable.

Note that this is entirely a matter of personal taste & I won't hold it against you :)

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 one is similar to the one above, but different in one important way. As I mentioned there, I don't really have a personal preference, all I would lobby for is for us to share a single formatter configuration—my general philosophy is that I usually don't care about these things as long as there's one way to do it and we all follow, whatever this way might be.

All that said, my editor right now wraps at 120 columns and the Effective Scala guide mentions 100 columns as the standard:

Indent by two spaces. Try to avoid lines greater than 100 columns in length. Use one blank line between method, class, and object definitions.

What rule should we follow, wrap at 100, always wrap...?

val allStringAttributes = attributes.filter(_.attributeType == StringAttribute).map(_.asInstanceOf[IstioAttribute[String]])
val allInt64Attributes = attributes.filter(_.attributeType == Int64Attribute).map(_.asInstanceOf[IstioAttribute[Long]])
val allStringMapAttributes = attributes.filter(_.attributeType == StringMapAttribute).map(_.asInstanceOf[IstioAttribute[Map[String, String]]])
val allDurationAttributes = attributes.filter(_.attributeType == DurationAttribute).map(_.asInstanceOf[IstioAttribute[Duration]])
Copy link
Member

Choose a reason for hiding this comment

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

Is there any way to do this w/o casting?

Copy link
Contributor Author

@pcalcado pcalcado Sep 26, 2017

Choose a reason for hiding this comment

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

I looked at this for a bit and couldn't think of anything, due to attributes: Seq[IstioAttribute[_]]. Did I miss something?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. What about a function on IstioAttribute taking an attribute type and optionally returning that attribute if it is of the given type?

Copy link
Member

Choose a reason for hiding this comment

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

Another potential concern here – which might be a premature optimization – is that the repeated calls to filter mean that we traverse the sequence of attributes 4 times.

As an alternative, consider using mutable collections (gasp!) and traversing the sequence a single time and appending the attribute to a different collection for each attribute type? If this is in the hot path, it might be worth using mutability.

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, if you really want to avoid mutable collections, could always do something like this

val (stringAttrOpts, int64AttrOpts, stringMapAttrOpts, durationAttrOpts) = 
  attributes.map { 
    case attr: IstioAttribute[String] => (Some(attr), None, None, None)
    case attr: IstioAttribute[Long] => (None, Some(attr), None, None)
    case attr: IstioAttribute[Map[String, String]] => (None, None, Some(attr), None)
    case attr: IstioAttribute[Duration] => (None, None, None, Some(attr))
  }.unzip
// the resultant collections are now `Seq[Option[IstioAttribute[T]]]`, which you'd then have to `.flatten`...

which is kind of ugly, but is purely functional!

Copy link
Member

Choose a reason for hiding this comment

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

With the change to IstioAttributes I suggest above, this could be something like:

val allStringAttributes = attributes.collect { case attr@StringAttribute(_) => attr }

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've made the changes @adleong mentioned. @hawkw that's a valid concern and interesting solution, but I would argue that we should rather get through with istio 0.1.x features and address performance on our 0.2 work. Istio 0.1.x doesn't focus on performance as much and I find it unlikely that this would be an actual bottleneck as opposed to, for e.g. the lack of caching in this release.

CheckRequest(TODO, Some(attributesUpdate))
}

private def mkDictionary(
Copy link
Member

Choose a reason for hiding this comment

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

This whole function seems really convoluted, is there any way it can be simplified or broken apart?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ReportRequest(attributeUpdate = Some(updated))
}

def mkCheckRequest(istioREquest: IstioRequest[_]) = {
Copy link
Member

Choose a reason for hiding this comment

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

is there a reason for the capitalization in istioREquest?

Copy link
Member

Choose a reason for hiding this comment

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

Also, TIOLI, but it seems to me that a lot of this function might be better represented as a series of functions on IstioRequest?

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, the reason is that I am still not used to the new mac's keyboard and keep fat fingering everything -_-

Fixed it, thanks :)

On the second comment, I think that most of this stuff is only relevant in a Mixer API context, and I would not like to create any coupling between the request object and Mixer. What did you have in mind?

@pcalcado pcalcado force-pushed the phil/1433-istio-refactoring branch 7 times, most recently from d1ec4df to 152166e Compare September 26, 2017 03:43
val allStringAttributes = attributes.filter(_.attributeType == StringAttribute).map(_.asInstanceOf[IstioAttribute[String]])
val allInt64Attributes = attributes.filter(_.attributeType == Int64Attribute).map(_.asInstanceOf[IstioAttribute[Long]])
val allStringMapAttributes = attributes.filter(_.attributeType == StringMapAttribute).map(_.asInstanceOf[IstioAttribute[Map[String, String]]])
val allDurationAttributes = attributes.filter(_.attributeType == DurationAttribute).map(_.asInstanceOf[IstioAttribute[Duration]])
Copy link
Member

Choose a reason for hiding this comment

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

Another potential concern here – which might be a premature optimization – is that the repeated calls to filter mean that we traverse the sequence of attributes 4 times.

As an alternative, consider using mutable collections (gasp!) and traversing the sequence a single time and appending the attribute to a different collection for each attribute type? If this is in the hot path, it might be worth using mutability.

val allStringAttributes = attributes.filter(_.attributeType == StringAttribute).map(_.asInstanceOf[IstioAttribute[String]])
val allInt64Attributes = attributes.filter(_.attributeType == Int64Attribute).map(_.asInstanceOf[IstioAttribute[Long]])
val allStringMapAttributes = attributes.filter(_.attributeType == StringMapAttribute).map(_.asInstanceOf[IstioAttribute[Map[String, String]]])
val allDurationAttributes = attributes.filter(_.attributeType == DurationAttribute).map(_.asInstanceOf[IstioAttribute[Duration]])
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, if you really want to avoid mutable collections, could always do something like this

val (stringAttrOpts, int64AttrOpts, stringMapAttrOpts, durationAttrOpts) = 
  attributes.map { 
    case attr: IstioAttribute[String] => (Some(attr), None, None, None)
    case attr: IstioAttribute[Long] => (None, Some(attr), None, None)
    case attr: IstioAttribute[Map[String, String]] => (None, None, Some(attr), None)
    case attr: IstioAttribute[Duration] => (None, None, None, Some(attr))
  }.unzip
// the resultant collections are now `Seq[Option[IstioAttribute[T]]]`, which you'd then have to `.flatten`...

which is kind of ugly, but is purely functional!

)
log.trace("MixerClient.report: %s", reportRequest)
client.report(Stream.value(reportRequest))
Future.Done
Copy link
Member

Choose a reason for hiding this comment

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

Should this return a finished Future, or return a future that's satisfied when the report stream finishes?

Copy link
Member

Choose a reason for hiding this comment

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

if this is supposed to be fire-and-forget, maybe the return type of this method should just be Unit?

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's definitely fire-and-forget, and because nobody cares about when it's completed I decided to just return a constant. On the return type, I generally like methods from a single type to be symmetric, i.e. given a set A it always map from A to S[A], but no strong preference.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm... I don't understand your comment about symmetric return types. What would be the difference between having the return type be Unit instead of Future[Unit]?

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 mean that I would expect all functions in a given type/namespace to return values of the same container, or at least the same kind. In this particular case, it means that I would expect the published interface of MixerClient to be either this:

class MixerClient(client: Mixer) {
  def report(/* ... */): Future[Unit]
  def checkPreconditions(/* ... */): Future[MixerCheckStatus]
}

Or:

class MixerClient(client: Mixer) {
  def report(/* ... */): Unit
  def checkPreconditions(/* ... */): MixerCheckStatus
}

But not:

class MixerClient(client: Mixer) {
  def report(/* ... */): Unit
  def checkPreconditions(/* ... */): Future[MixerCheckStatus]
}

But, again, this is just a preference, I'm happy to make it Unit if it makes it more aligned with our internal philosophy.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. I think Unit is more aligned with my personal philosophy anyway. (since this method is fire-and-forget)

)
log.trace("MixerClient.report: %s", reportRequest)
client.report(Stream.value(reportRequest))
Future.Done
Copy link
Member

Choose a reason for hiding this comment

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

if this is supposed to be fire-and-forget, maybe the return type of this method should just be Unit?

import ClusterCache._
case class Cluster(dest: String, port: String)

trait ClusterCache extends Closable {
Copy link
Member

Choose a reason for hiding this comment

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

I have a lot of opinions about testing that we can discuss at great length sometime in the future. But I think this is fine.

def get(domain: String): Future[Option[Cluster]]
}

class ClusterCacheBackedByApi(client: DiscoveryClient) extends ClusterCache {
Copy link
Member

Choose a reason for hiding this comment

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

I don't have a super strong opinion about naming. I have a mild preference for a more concise name. Using package namespace to differentiate would also be fine.

import io.buoyant.config.types.Port
import io.buoyant.k8s.istio.mixer.MixerClient

object IstioServices {
Copy link
Member

Choose a reason for hiding this comment

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

rename file to IstioServices.scala?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

import io.buoyant.k8s.istio.mixer.MixerClient

object IstioServices {
protected val log = Logger.get(this.getClass.getSimpleName)
Copy link
Member

Choose a reason for hiding this comment

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

I think this is equivalent to Logger() which is more 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.

Aye

val allStringAttributes = attributes.filter(_.attributeType == StringAttribute).map(_.asInstanceOf[IstioAttribute[String]])
val allInt64Attributes = attributes.filter(_.attributeType == Int64Attribute).map(_.asInstanceOf[IstioAttribute[Long]])
val allStringMapAttributes = attributes.filter(_.attributeType == StringMapAttribute).map(_.asInstanceOf[IstioAttribute[Map[String, String]]])
val allDurationAttributes = attributes.filter(_.attributeType == DurationAttribute).map(_.asInstanceOf[IstioAttribute[Duration]])
Copy link
Member

Choose a reason for hiding this comment

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

With the change to IstioAttributes I suggest above, this could be something like:

val allStringAttributes = attributes.collect { case attr@StringAttribute(_) => attr }

}

/**
* Checks this request agains ixer' Pre-Condition system, using the [[https://istio.io/docs/reference/api/mixer/mixer-service.html#check Check API]]
Copy link
Member

Choose a reason for hiding this comment

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

typo: ixer'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

}

private[this] def mkStatus(stream: Stream.Releasable[CheckResponse]) = {
val result = stream.value.`result`.get
Copy link
Member

Choose a reason for hiding this comment

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

Instead of get and getOrElse, I think we can chain all these options together with flatMap and then convert to a Future with Future.const

Copy link
Member

Choose a reason for hiding this comment

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

we also have to call stream.release()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the chaining, I am not sure I understand: each option can be present or not, and if not we're using the default value. How would we deal with the default values on a flatMap chain?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, and on the release() comment, do we have to manually release each stream item in our gRPC implementation? The current implementation seems to be a noop and I couldn't find an example of something else consuming gRPC like this, so I'm not super sure what's the usual idiom.

Future.value(MixerCheckStatus(codeUsedWhenUnknownStatus))
}

private[this] def mkStatus(stream: Stream.Releasable[CheckResponse]) = {
Copy link
Member

Choose a reason for hiding this comment

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

I think a Stream.Releasable is an item from the stream, not the stream itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

import io.buoyant.k8s.istio.{CurrentIstioPath, IstioRequest}

object HttpIstioRequest {
def apply(req: Request): IstioRequest[Request] =
Copy link
Member

Choose a reason for hiding this comment

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

I think we should pass Option[IstioPath] in here so that callers are explicit about when they are pulling the local context.

Copy link
Member

Choose a reason for hiding this comment

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

This means that code in the identifiers that construct an HttpIstioRequest can explicitly pass None for IstioPath

@pcalcado pcalcado force-pushed the phil/1433-istio-refactoring branch 5 times, most recently from 82fb2d9 to fd1cccc Compare September 28, 2017 17:31
Copy link
Member

@adleong adleong left a comment

Choose a reason for hiding this comment

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

Thanks for putting up with all my nit-picking! This is looking great! I'd like to deploy it to GKE and poke at it a bit since it's such a large change. I should have time to do that tomorrow morning.

description: String
) extends IstioAttribute[Duration](name, description)

case class SourceIpIstioAttribute(val value: String)
Copy link
Member

Choose a reason for hiding this comment

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

no need for val on case class params


abstract class IstioRequestAuthorizerFilter[Req, Resp](mixerClient: MixerClient) extends Filter[Req, Resp, Req, Resp] {

def report(request: IstioRequest[Req], response: IstioResponse[Resp], duration: Duration) = {
Copy link
Member

Choose a reason for hiding this comment

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

I like explicit return type on public methods

object IstioServices {
protected val log = Logger()

def mkMixerClient(mixerHost: Option[String], mixerPort: Option[Port]) = {
Copy link
Member

Choose a reason for hiding this comment

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

I like explicit return types on public methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Me too, will fix it. I think Intellij has a setting for this somewhere?

)
log.trace("MixerClient.report: %s", reportRequest)
client.report(Stream.value(reportRequest))
Future.Done
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. I think Unit is more aligned with my personal philosophy anyway. (since this method is fire-and-forget)

val istioIdentifier = new InternalTrafficIdentifier[Request](pfx, baseDtab, routeCache, clusterCache, mixerClient, new H2IstioRequestHandler)

override def apply(originalRequest: Request): Future[RequestIdentification[Request]] = {
val req = H2IstioRequest(originalRequest, None)
Copy link
Member

Choose a reason for hiding this comment

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

😍 explicit None

Copy link
Member

Choose a reason for hiding this comment

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

this phrase gives me unpleasant FORTRAN flashbacks

@pcalcado pcalcado force-pushed the phil/1433-istio-refactoring branch 2 times, most recently from a1ab2ff to de759da Compare September 29, 2017 14:35
@pcalcado
Copy link
Contributor Author

@adleong cool! In that case, I'd recommend we also review/merge and test #1606 , as the only difference between that PR and this one is this commit: b9dd7df

Copy link
Member

@adleong adleong left a comment

Choose a reason for hiding this comment

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

⭐ 🎚 🍄

@pcalcado pcalcado merged commit 62f853c into master Oct 12, 2017
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.

3 participants