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

Implement Composer group repositories #14

Merged
merged 32 commits into from
Jun 12, 2018
Merged

Implement Composer group repositories #14

merged 32 commits into from
Jun 12, 2018

Conversation

fjmilens3
Copy link
Contributor

@fjmilens3 fjmilens3 commented Apr 6, 2018

Fixes #8 by adding basic support for merging (flattened) packages.json files and provider JSON files, along with the standard group handler for obtaining the correct zip file based on repository ordering.

@fjmilens3 fjmilens3 added the wip Work in progress label Apr 6, 2018
@fjmilens3 fjmilens3 self-assigned this Apr 6, 2018
@fjmilens3 fjmilens3 removed the wip Work in progress label Apr 10, 2018
@TheBay0r
Copy link
Contributor

Hey @fjmilens3 ! I just gave your branch a shot. I have composer (group) now in the recipes, but I can not select it. Not sure if I'm to early with testing it, but just wanted to let you know 🙂

@fjmilens3
Copy link
Contributor Author

@TheBay0r, that concerns me somewhat because I'm able to select the composer (group) repo and create one without an issue. A couple of questions:

  1. Do you get any error message/popup/log entry when you try to click on the "composer (group)" entry in the list?
  2. Have you tried doing a hard refresh in your browser or clearing the browser cache?

@TheBay0r
Copy link
Contributor

TheBay0r commented Apr 18, 2018

@fjmilens3, you are right. I was able to create a grouped repository now. I decided to combine composer-proxy and composer-hosted and decided to just name it composer. In the user interface it is fine and I can see all the packages from proxy and hosted.

When I try to do a composer install using the grouped repository only, I retrieve a NullPointerException. Accessing the same package via composer-hosted works

2018-04-18 08:07:52,287+0000 WARN  [qtp1963253024-60] *UNKNOWN org.sonatype.nexus.repository.httpbridge.internal.ViewServlet - Failure servicing: GET /repository/composer/{private_vendor}/{private_package}/{version}/{filename}.zip
java.lang.NullPointerException: null
	at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:770)
	at org.sonatype.nexus.repository.composer.internal.ComposerProxyFacetImpl.getZipballUrl(ComposerProxyFacetImpl.java:174)
	at org.sonatype.nexus.repository.composer.internal.ComposerProxyFacetImpl.getUrl(ComposerProxyFacetImpl.java:133)
	at org.sonatype.nexus.repository.proxy.ProxyFacetSupport.fetch(ProxyFacetSupport.java:345)
	at org.sonatype.nexus.repository.proxy.ProxyFacetSupport.doGet(ProxyFacetSupport.java:219)
	at org.sonatype.nexus.repository.proxy.ProxyFacetSupport.lambda$1(ProxyFacetSupport.java:208)
	at org.sonatype.nexus.repository.proxy.Cooperation$CooperatingFuture.download(Cooperation.java:259)
	at org.sonatype.nexus.repository.proxy.Cooperation.download(Cooperation.java:191)
	at org.sonatype.nexus.repository.proxy.Cooperation.cooperate(Cooperation.java:90)
	at org.sonatype.nexus.repository.proxy.ProxyFacetSupport.get(ProxyFacetSupport.java:200)
	at org.sonatype.nexus.repository.proxy.ProxyHandler.handle(ProxyHandler.java:49)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.storage.UnitOfWorkHandler.handle(UnitOfWorkHandler.java:39)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.ContentHeadersHandler.handle(ContentHeadersHandler.java:44)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.http.PartialFetchHandler.handle(PartialFetchHandler.java:55)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.ConditionalRequestHandler.handle(ConditionalRequestHandler.java:72)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.cache.NegativeCacheHandler.handle(NegativeCacheHandler.java:50)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.assetdownloadcount.internal.AssetDownloadCountContributedHandler.handle(AssetDownloadCountContributedHandler.java:53)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.HandlerContributor.handle(HandlerContributor.java:67)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.ExceptionHandler.handle(ExceptionHandler.java:44)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.security.SecurityHandler.handle(SecurityHandler.java:52)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.Context$proceed.call(Unknown Source)
	at org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport$_closure1.doCall(ComposerRecipeSupport.groovy:110)
	at sun.reflect.GeneratedMethodAccessor221.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1087)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at groovy.lang.Closure.call(Closure.java:414)
	at org.codehaus.groovy.runtime.ConvertedClosure.invokeCustom(ConvertedClosure.java:54)
	at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:124)
	at com.sun.proxy.$Proxy157.handle(Unknown Source)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.TimingHandler.handle(TimingHandler.java:46)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.Context.start(Context.java:114)
	at org.sonatype.nexus.repository.view.Router.dispatch(Router.java:63)
	at org.sonatype.nexus.repository.view.ConfigurableViewFacet.dispatch(ConfigurableViewFacet.java:52)
	at org.sonatype.nexus.repository.group.GroupHandler.getFirst(GroupHandler.java:120)
	at org.sonatype.nexus.repository.group.GroupHandler.doGet(GroupHandler.java:97)
	at org.sonatype.nexus.repository.group.GroupHandler.handle(GroupHandler.java:81)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.assetdownloadcount.internal.AssetDownloadCountContributedHandler.handle(AssetDownloadCountContributedHandler.java:53)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.HandlerContributor.handle(HandlerContributor.java:67)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.ExceptionHandler.handle(ExceptionHandler.java:44)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.security.SecurityHandler.handle(SecurityHandler.java:52)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.Context$proceed.call(Unknown Source)
	at org.sonatype.nexus.repository.composer.internal.ComposerRecipeSupport$_closure1.doCall(ComposerRecipeSupport.groovy:110)
	at sun.reflect.GeneratedMethodAccessor221.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1087)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at groovy.lang.Closure.call(Closure.java:414)
	at org.codehaus.groovy.runtime.ConvertedClosure.invokeCustom(ConvertedClosure.java:54)
	at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:124)
	at com.sun.proxy.$Proxy157.handle(Unknown Source)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.handlers.TimingHandler.handle(TimingHandler.java:46)
	at org.sonatype.nexus.repository.view.Context.proceed(Context.java:80)
	at org.sonatype.nexus.repository.view.Context.start(Context.java:114)
	at org.sonatype.nexus.repository.view.Router.dispatch(Router.java:63)
	at org.sonatype.nexus.repository.view.ConfigurableViewFacet.dispatch(ConfigurableViewFacet.java:52)
	at org.sonatype.nexus.repository.view.ConfigurableViewFacet.dispatch(ConfigurableViewFacet.java:43)
	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.dispatchAndSend(ViewServlet.java:211)
	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.doService(ViewServlet.java:173)
	at org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.service(ViewServlet.java:126)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at com.google.inject.servlet.ServletDefinition.doServiceImpl(ServletDefinition.java:286)
	at com.google.inject.servlet.ServletDefinition.doService(ServletDefinition.java:276)
	at com.google.inject.servlet.ServletDefinition.service(ServletDefinition.java:181)
	at com.google.inject.servlet.DynamicServletPipeline.service(DynamicServletPipeline.java:71)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:85)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:112)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
	at org.sonatype.nexus.security.SecurityFilter.executeChain(SecurityFilter.java:85)
	at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
	at org.sonatype.nexus.security.SecurityFilter.doFilterInternal(SecurityFilter.java:101)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at org.sonatype.nexus.repository.httpbridge.internal.ExhaustRequestFilter.doFilter(ExhaustRequestFilter.java:71)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at com.sonatype.nexus.licensing.internal.LicensingRedirectFilter.doFilter(LicensingRedirectFilter.java:108)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at com.codahale.metrics.servlet.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:97)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at org.sonatype.nexus.internal.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:68)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at org.sonatype.nexus.internal.web.EnvironmentFilter.doFilter(EnvironmentFilter.java:102)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at org.sonatype.nexus.internal.web.HeaderPatternFilter.doFilter(HeaderPatternFilter.java:98)
	at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
	at com.google.inject.servlet.DynamicFilterPipeline.dispatch(DynamicFilterPipeline.java:104)
	at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:135)
	at org.sonatype.nexus.bootstrap.osgi.DelegatingFilter.doFilter(DelegatingFilter.java:73)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1629)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:533)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:190)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1595)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:188)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1253)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:168)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1564)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:166)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1155)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at com.codahale.metrics.jetty9.InstrumentedHandler.handle(InstrumentedHandler.java:175)
	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:126)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at org.eclipse.jetty.server.Server.handle(Server.java:530)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:347)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:256)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:102)
	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:247)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:140)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:382)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:708)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:626)
	at java.lang.Thread.run(Thread.java:748)

@fjmilens3
Copy link
Contributor Author

When I try to do a composer install using the grouped repository only, I retrieve a NullPointerException. Accessing the same package via composer-hosted works

@TheBay0r, this is a problem caused by the hackery I did in the proxy implementation to look up the provider JSON for a particular vendor and project in order to obtain the dist url. This probably wouldn't happen if you had your hosted repo before your proxy repo in the configuration, but it's still something we need to fix because it manifests a larger issue. The short version is that we're not robustly dealing with certain download situations in proxy (where a file that does not exist is requested, but it still checks upstream to reference any existing provider JSON first).

For some background, Nexus Repo does group merge in two ways. For metadata, indices, and the like, it has to fetch all upstream responses from the member repos and merge them together. For actual content, we basically just hit each repository in succession until we find something and return that as the result. (You can see https://github.com/sonatype/nexus-public/blob/ffc3d00c05778ee415bfa69135d982bac2f38450/components/nexus-repository/src/main/java/org/sonatype/nexus/repository/group/GroupHandler.java and subclasses as the crown jewel of this particular dynasty.)

In this case, you're running into a quirk of the Composer implementation I've done. The merge for all the metadata/indices works just fine, but when you're to fetching the zip archive, the code in proxy goes and tries to find the provider JSON to obtain the url indirectly in case it hasn't been stored yet or needs updated (this is largely a result of how the proxy support works in Nexus Repo). In that case, we're not dealing with the missing provider content gracefully, and the result is the stack trace you see before you.

I have a pretty good idea of how to fix this as some other formats (npm) end up with similar problems, fortunately, and as today is our "improvement day" I think I can get something together quickly. I can't thank you enough for how involved you've been in contributing feedback and testing, it counts a lot to know what we do is valued!

@fjmilens3
Copy link
Contributor Author

@TheBay0r, when you get a chance, give it a try with 95a20b5 and see if that works any better for your scenario.

@TheBay0r
Copy link
Contributor

@fjmilens3 I'm sry that it took me so long to have another look at it. And I'm happy when my little testing helps you. Wish I could do even more, it is a great plugin 🙂

Downloading the packages.json works with your fix. I tried downloading the file manually and just had a quick look and also tried installing the dependencies using the grouped repository only. I didn't have any problem that would be related to the composer-group branch! So your fix looks good for me 🙂

if (successfulResponses.isEmpty()) {
return notFoundResponse(context);
}
List<Payload> payloads = successfulResponses.stream().map(Response::getPayload).collect(Collectors.toList());
Copy link

Choose a reason for hiding this comment

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

Question: Can this map not be appended to the filter above? then you can still check for empty list, else merge and respond ok

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: Can this map not be appended to the filter above? then you can still check for empty list, else merge and respond ok

We can do that, mostly just following the convention from earlier formats where we'd filter by status, check, and then process the payloads. Particularly as we keep trying to open source formats, keeping things idiomatically consistent for the same operations could make it easier for people to read the code and contribute. Changed in 6d08022.

if (successfulResponses.isEmpty()) {
return notFoundResponse(context);
}
List<Payload> payloads = successfulResponses.stream().map(Response::getPayload).collect(Collectors.toList());
Copy link

Choose a reason for hiding this comment

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

Same question applies here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same question applies here

Same answer. :) Done in 8b43ec5.

@Nonnull final GroupHandler.DispatchedRepositories dispatched)
throws Exception
{
Repository repository = context.getRepository();
Copy link

Choose a reason for hiding this comment

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

Suggestion: This is a repeat of ComposerGroupPackagesJsonHandler#doGet, maybe you could create a common method and pass in a lambda which is the merge

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggestion: This is a repeat of ComposerGroupPackagesJsonHandler#doGet, maybe you could create a common method and pass in a lambda which is the merge

The lambda idea isn't my favorite for two reasons:

  • We inject these into the Recipes so that's not as straightforward as you might think.
  • Lambdas are nice for smaller bits of code such as filtering a collection, but their relative anonymity in a larger codebase can make it harder to find all the pieces you're looking for.

What do you think of 8f727aa instead?

Copy link

Choose a reason for hiding this comment

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

I like :)

*/
public Content mergePackagesJson(final Repository repository, final List<Payload> payloads) throws IOException {
Set<String> names = new HashSet<>();
for (Payload payload : payloads) {
Copy link

Choose a reason for hiding this comment

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

Trivial: I think you could consider using functional approach here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trivial: I think you could consider using functional approach here

The reason I didn't do that was because we'd end up having to go the route of using UncheckedIOException because of the difficulty of throwing checked exceptions from within the lambdas in the stream.

String time = now.withZone(DateTimeZone.UTC).toString(timeFormatter);

// TODO: Make this more robust, right now it makes a lot of assumptions and doesn't deal with bad things well,
// can probably consolidate this with the handling for rewrites for proxy (or at least make it more rational).
Copy link

Choose a reason for hiding this comment

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

Suggestion: If possible I would consider breaking it down so it would be more manageable for someone else to visit

Copy link
Contributor

Choose a reason for hiding this comment

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

I would at least break out the individual concerns i.e. buildDistInfo, buildPackageInfo

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 would at least break out the individual concerns i.e. buildDistInfo, buildPackageInfo

Done, also consolidated some near-duplicate code in 5ae0913.

Copy link
Contributor

@j-s-3 j-s-3 left a comment

Choose a reason for hiding this comment

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

Approach looks good, some minor comments/questions.

}

@Override
protected Response doGet(@Nonnull final Context context,
Copy link
Contributor

Choose a reason for hiding this comment

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

Trivial: I can't remember exactly but I thought we agreed that everything was considered non-null unless marked Nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trivial: I can't remember exactly but I thought we agreed that everything was considered non-null unless marked Nullable?

We did, but the superclass defines @Nonnull so IDEA complains that the annotation has been removed (as analysis of the code would think we have a lesser restriction on nullability here than in the parent class). Until we take that out I'd rather do this than get warnings from IDEs or from static analysis.

GroupFacet groupFacet = repository.facet(GroupFacet.class);
Map<Repository, Response> responses = getAll(context, groupFacet.members(), dispatched);
List<Response> successfulResponses = responses.values().stream()
.filter(response -> response.getStatus().getCode() == HttpStatus.OK && response.getPayload() != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you stripping out the conditional headers somewhere? i.e. if someone passes an if-not-modified to the group endpoint will we then pass that to the member repositories possibly resulting in a NOT_MODIFIED 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.

Are you stripping out the conditional headers somewhere?

Good catch, done in 788b51a.

GroupFacet groupFacet = repository.facet(GroupFacet.class);
Map<Repository, Response> responses = getAll(context, groupFacet.members(), dispatched);
List<Response> successfulResponses = responses.values().stream()
.filter(response -> response.getStatus().getCode() == HttpStatus.OK && response.getPayload() != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar question to the packages handler about conditional gets

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar question to the packages handler about conditional gets

Done in 788b51a.


builder.route(packagesMatcher()
.handler(timingHandler)
.handler(assetKindHandler.rcurry(AssetKind.PACKAGES))
Copy link
Contributor

Choose a reason for hiding this comment

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

Trivial: no reason not to statically import these asset kinds.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trivial: no reason not to statically import these asset kinds.

I left them without static imports in the other recipes; to me it does provide a bit of useful context, but I'm not averse to changing them as part of a tidying PR at some point.

.putString(time, StandardCharsets.UTF_8)
.hash()
.asInt());
pkg.put(UID_KEY, Integer.toUnsignedLong(
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: What was the reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: What was the reason for this change?

It's somewhat of a long story, but basically I didn't want to have negative values for UIDs.

newPackageInfo.put(TIME_KEY, time);
newPackageInfo.put(UID_KEY, Integer.toUnsignedLong(
Hashing.md5().newHasher()
.putString(packageName, StandardCharsets.UTF_8)
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: As we do this twice a utility method that takes var args would make it clearer i.e. calculateMd5HashInteger(String... input);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Minor: As we do this twice a utility method that takes var args would make it clearer i.e. calculateMd5HashInteger(String... input);

Consolidated to a single usage as part of 5ae0913.


Map<String, Object> newDistInfo = new LinkedHashMap<>();
newDistInfo.put(URL_KEY, String
.format(REWRITE_URL, repository.getUrl(), packageName, packageVersion, packageName.replace('/', '-'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: Although simple this would make a good util method to make refactoring easier in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Minor: Although simple this would make a good util method to make refactoring easier in the future.

Related change in 88f058a (also makes the output between proxy, hosted, and group more consistent for dist portions).

String time = now.withZone(DateTimeZone.UTC).toString(timeFormatter);

// TODO: Make this more robust, right now it makes a lot of assumptions and doesn't deal with bad things well,
// can probably consolidate this with the handling for rewrites for proxy (or at least make it more rational).
Copy link
Contributor

Choose a reason for hiding this comment

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

I would at least break out the individual concerns i.e. buildDistInfo, buildPackageInfo

}
}

return new Content(new StringPayload(mapper.writeValueAsString(Collections.singletonMap(PACKAGES_KEY, packages)),
Copy link
Contributor

Choose a reason for hiding this comment

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

Trivial: Collections doesn't provide any additional information here you could safely statically import it (i.e. In fact a map isn't strictly a "Collection" ... I wonder why they put the method there hmm).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trivial: Collections doesn't provide any additional information here you could safely statically import it (i.e. In fact a map isn't strictly a "Collection" ... I wonder why they put the method there hmm).

Done in 70805ad.

return super.fetch(context, stale);
}
catch (NonResolvableProviderJsonException e) {
log.debug("Composer provider URL not resolvable: {}", e.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: What's the reason for this being debug and not error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: What's the reason for this being debug and not error?

Because it's not always an error, for example in the case that a proxy repo is being queried before a hosted repo that does contain an artifact; in that case the exception path is actually quite common, much like the npm implementation (in that it has to go remote to see if there's even a file containing the download url, and we have to have a way to indicate that within the confines of the formats framework we have).

See 95a20b5 and the comments around #14 (comment) and #14 (comment) for details.

Copy link

@doddi doddi left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@j-s-3 j-s-3 left a comment

Choose a reason for hiding this comment

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

👍

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 6, 2018

I'm just about to build that branch and will test it this week. I'll let you know how it looks as soon as possible 🙂

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 7, 2018

@fjmilens3 it looks like there is some regression. 🤔

The composer-lock is missing a couple of informations now, that were there in the composer-hosted branch still. (Although the information is coming from composer-proxy). I also tried just using the composer-proxy without the composer-group feature as well.

As you can see as an example for phpunit in the screenshot it is missing the information of the binary, which causes the vendor/bin folder to not be created and therefore vendor/bin/phpunit to not be available.

Also the extra part is missing were I'm not sure what it is used for, next to some meta information below.

What is also a little bit concerning is, that we lost the reference for the package. Which means if somebody (for whatever) reason would puplish the same version with a different commit hash, composer wouldn't realise that there was a change.

screen shot 2018-06-07 at 09 37 45

Hope it is readable enough for you. If you have any questions, just let me know.

I even deployed latest master again to verify that this information wasn't missing after adding the composer-hosted feature.

@fjmilens3
Copy link
Contributor Author

@TheBay0r, I've taken a quick look at this and made what I think are the necessary changes, but I have not tested it beyond unit testing. I'll probably try and take a look at this for further testing sometime next week as we're traveling this weekend, but you may end up getting to it before I do (and probably have more context than myself by far, in that I'd trust you more than I'd trust myself to find things wrong with it).

As far as what happened here, as part of working on group I tried to be more consistent in terms of how we handled different fields in building and merging JSON, and the minimal subset that I chose was too minimal to support some of these additional features; when I do test this I'm usually testing against very trivial composer.json files rather than building a huge project, so a lot of that sneaks through. Note that to be consistent with adding (or re-adding) some of these fields, I've also gone ahead and added them for hosted since they were missing there.

Also note that I added your name to our contributors list on b3e9b80 because of all the QA work you've done on this project over these months; if you want me to take that out I will, but I'd like to recognize your support if you're okay with that.

@@ -23,4 +23,4 @@ Sonatype internal people, in alphabetical order by username:

External contributors:

![Possibly You!](http://i.imgur.com/A3eScYul.jpg)
* [@TheBay0r](https://github.com/TheBay0r) (Stefan Schacherl)
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a real honor, thank you :)

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 8, 2018

I can verify that the regression issue is seems to be resolved, although I currently have problems building one of our containers. I'll have to take a deeper look and will let you know if it is related to this branch.

Something to clarify in the meanwhile. I understood you added the missing fields to composer-hosted as well. But when I have a look at one of our private packages, it doesn't look like it. See screenshot:
screen shot 2018-06-08 at 11 37 14
(sorry that I anonymise a lot, guess it is still readable 🙂 )

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 8, 2018

Okay… it seems to be that the failing build is related to the "target-dir" property still missing the JSON.

screen shot 2018-06-08 at 12 01 07

@fjmilens3
Copy link
Contributor Author

Something to clarify in the meanwhile. I understood you added the missing fields to composer-hosted as well. But when I have a look at one of our private packages, it doesn't look like it.

@TheBay0r, I will take a look and test some hosted uploads myself to see what's going on. The changes only affect newly-deployed packages, was this one that you deployed after these changes or one that you had around previously in your Nexus storage?

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 8, 2018

The changes only affect newly-deployed packages, was this one that you deployed after these changes or one that you had around previously in your Nexus storage?

I already thought that question would come up, so I tested a package that was there before and also a package I just uploaded. Although I am not sure if it might interfere because I just replaced an existing version?

@fjmilens3
Copy link
Contributor Author

I already thought that question would come up, so I tested a package that was there before and also a package I just uploaded. Although I am not sure if it might interfere because I just replaced an existing version?

@TheBay0r:

It shouldn't, if it did that, then it's a bug (my first thought would center on whether or not we're correctly handling update events in terms of regenerating the metadata files).

For what it's worth, I've tried it locally with a new upload and it seems to be working correctly (I've also added a few more fields to be stored), so I'm wondering if there is something more going on here.

Could you put together a sample composer.json or entire package, suitably anonymized, but based on yours, so that I can use that in my testing (for upload purposes)? I only had a few minutes to look at this between tasks today at work, so that might help me work it through when I do have more time.

@TheBay0r
Copy link
Contributor

TheBay0r commented Jun 8, 2018

@fjmilens3 I just tried a few more things.
Re-uploading an existing package -> Downloading the JSON from /p/vendor/{package}.json
=> None of the new fields is available

Uploading a completely new version of the package -> Downloading JSON from /p/vendor/{package}.json
=> All previous versions also have the new fields, not just the one just uploaded

But I still have to have a look at fcbc942 because those fields were obviously still missing.

Providing you with a full composer.json might take me until Monday

@TheBay0r
Copy link
Contributor

I guess one of the best composer.json you can find is actually the one from the composer repo itself. I just extendid it slightly by adding the repository field and adding an additional way for scripts. Also I removed the names. But I guess this covers most of the fields 🤔

{
    "name": "composer/composer",
    "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
    "keywords": ["package", "dependency", "autoload"],
    "homepage": "https://getcomposer.org/",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Not Person",
            "email": "not.person@examplehomepagenotvalid.it",
            "homepage": "http://www.examplehomepagenotvalid.it"
        },
        {
            "name": "Another Not Person",
            "email": "another@examplehomepagenotvalid.it",
            "homepage": "http://www.examplehomepagenotvalid.it"
        }
    ],
    "support": {
        "irc": "irc://irc.examplehomepagenotvalid.it/composer",
        "issues": "https://github.com/composer/composer/issues"
    },
    "repositories": [
        {
            "type": "git",
            "url": "https://github.com/foobar/intermediate.git",
            "no-api": true
        }
    ],
    "require": {
        "php": "^5.3.2 || ^7.0",
        "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
        "composer/ca-bundle": "^1.0",
        "composer/semver": "^1.0",
        "composer/spdx-licenses": "^1.2",
        "composer/xdebug-handler": "^1.1",
        "seld/jsonlint": "^1.4",
        "symfony/console": "^2.7 || ^3.0 || ^4.0",
        "symfony/finder": "^2.7 || ^3.0 || ^4.0",
        "symfony/process": "^2.7 || ^3.0 || ^4.0",
        "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
        "seld/phar-utils": "^1.0",
        "psr/log": "^1.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^4.8.35 || ^5.7",
        "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"
    },
    "conflict": {
        "symfony/console": "2.8.38"
    },
    "config": {
        "platform": {
            "php": "5.3.9"
        }
    },
    "suggest": {
        "ext-zip": "Enabling the zip extension allows you to unzip archives",
        "ext-zlib": "Allow gzip compression of HTTP requests",
        "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
    },
    "autoload": {
        "psr-4": { "Composer\\": "src/Composer" }
    },
    "autoload-dev": {
        "psr-4": { "Composer\\Test\\": "tests/Composer/Test" }
    },
    "bin": ["bin/composer"],
    "extra": {
        "branch-alias": {
            "dev-master": "1.7-dev"
        }
    },
    "scripts": {
        "test": "phpunit",
        "custom-cmd": [
            "chmod -R 777 *"
        ]
    }
}

@TheBay0r
Copy link
Contributor

Also had a look at fcbc942. It looks very good. The only thing is, that the .json generated in nexus is only updated when a new version is uploaded, but not when a existing version should be replaced 🤔

newPackageInfo.put(EXTRA_KEY, versionInfo.get(EXTRA_KEY));
}
if (versionInfo.containsKey(DESCRIPTION_KEY)) {
newPackageInfo.put(DESCRIPTION_KEY, versionInfo.get(DESCRIPTION_KEY));
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this is a duplicate for line 347-349

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this is a duplicate for line 347-349

Good catch! Removed in 9f29f63.

@fjmilens3
Copy link
Contributor Author

@TheBay0r:

I guess one of the best composer.json you can find is actually the one from the composer repo itself. I just extendid it slightly by adding the repository field and adding an additional way for scripts. Also I removed the names. But I guess this covers most of the fields

Thanks for getting that together, I ran it through once and it looks like we're in good shape. It did make me realize that I'm probably not handling merge correctly with respect to the time in for provider JSON files in groups, which I changed in 2780bc3 (if we use the time string from the JSON we're merging, we're more stable in terms of timestamps and UIDs than if we don't, in which case they'll be different for each and every request!).

Also had a look at fcbc942. It looks very good.

I appreciate you retesting those changes; at this point I'm going to merge this PR in as it should be sufficient for group.

The only thing is, that the .json generated in nexus is only updated when a new version is uploaded, but not when a existing version should be replaced

I want to do some more investigation on this as time permits, for now I'm breaking it out into #20 so we can discuss it more there; whatever is going on here is more related to hosted than to group so should probably be worked separately.

Thanks for all your help on this!

@fjmilens3 fjmilens3 merged commit 3cee91a into master Jun 12, 2018
@fjmilens3 fjmilens3 deleted the composer-group branch June 12, 2018 21:59
@sonatypecla
Copy link

sonatypecla bot commented Aug 6, 2020

Thanks for the contribution! Unfortunately we can't verify if the committer(s), Frederick John Milens III fmilens@sonatype.com, signed the CLA because they have not associated their commits with their GitHub user. Please follow these instructions to associate your commits with your GitHub user. Then sign the Sonatype Contributor License Agreement and this Pull Request will be revalidated.

@sonatypecla
Copy link

sonatypecla bot commented Apr 11, 2021

Thanks for the contribution! Unfortunately we can't verify if the committer(s), Frederick John Milens III fmilens@sonatype.com, signed the CLA because they have not associated their commits with their GitHub user. Please follow these instructions to associate your commits with your GitHub user. Then sign the Sonatype Contributor License Agreement and this Pull Request will be revalidated.

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.

Implement Composer group repositories
4 participants