q-shift / qshift-templates

Backstage templates for the Qshift demo
Apache License 2.0
1 stars 3 forks source link

Duplicate code in quarkus app generates endpoint clashes #48

Open aureamunoz opened 3 months ago

aureamunoz commented 3 months ago

The following project https://github.com/ch007m/ch007m-quarkus-app generated via backstage Quarkus plugin contains duplicated entities and resources. It comes from the recently generator code added for hibernate-orm-rest-data extension in Quarkus.

Duplicated code can be found in

The following error is launched when the application is started with mvn quarkus:devcommand:

io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#delete consumes *, produces *
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#delete consumes *, produces *
POST /my-entity is declared by :
io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#add consumes application/json, produces application/json
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#add consumes application/json, produces application/json
GET /my-entity is declared by :
io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#list consumes *, produces application/json
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#list consumes *, produces application/json
GET /my-entity is declared by :
io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#get consumes *, produces application/json
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#get consumes *, produces application/json
PUT /my-entity is declared by :
io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#update consumes application/json, produces application/json
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#update consumes application/json, produces application/json
GET /my-entity is declared by :
io.quarkus.demo.MyEntityResourceJaxRs_6379958a2e660cd15fde7de382353a8d80f383ae#count consumes *, produces application/json
io.quarkus.MyEntityResourceJaxRs_95036a537c257516229b105d011e9f48bae2e7cc#count consumes *, produces application/json

        at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.checkForDuplicateEndpoint(ResteasyReactiveProcessor.java:1379)
        at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.setupEndpoints(ResteasyReactiveProcessor.java:671)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:849)
        at io.quarkus.builder.BuildContext.run(BuildContext.java:256)
        at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
        at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
        at java.base/java.lang.Thread.run(Thread.java:1583)
        at org.jboss.threads.JBossThread.run(JBossThread.java:483)

cc @iocanel @cmoulliard

cmoulliard commented 3 months ago

As the template includes a step to enhance the skeleton of the project generated by code.quarkus.io, we should revisit the if condition which is currently based on the extension io.quarkus:quarkus-hibernate-orm-rest-data-panache

if: ${{ 'io.quarkus:quarkus-hibernate-orm-rest-data-panache' in parameters.extensions }}
      action: fetch:template
      input:
        url: skeletons/quarkus-orm-rest/
        replace: true

Could we check if the quarkus version is <= 3.9 and if the extension contains the word orm-rest-data-panache ?

@aureamunoz

cmoulliard commented 3 months ago

Could this syntax work if: ${{ 'io.quarkus:quarkus-hibernate-orm-rest-data-panache' in parameters.extensions and (parameters.quarkusVersion | split(':') | last | float) <= '3.9' }} ?

cmoulliard commented 3 months ago

Could this syntax work

No luck => 2024-04-04T10:36:55.463Z scaffolder error Failed to parse template string: ${{ 'io.quarkus:quarkus-hibernate-orm-rest-data-panache' in parameters.extensions and (parameters.quarkusVersion | split(':') | last | float) <= '3.9' }} with error (unknown path) Error: filter not found: split type=plugin

cmoulliard commented 3 months ago

I found how :-)

Such additional filters should be added to the scaffolder plugin

...
const actions = [
  createArgoCdResources( env.config, env.logger ),
  ...builtInActions,
  createQuarkusApp(),
  cloneQuarkusQuickstart()
];

return await createRouter({
  actions,
  logger: env.logger,
  config: env.config,
  database: env.database,
  reader: env.reader,
  catalogClient,
  identity: env.identity,
  permissions: env.permissions,
  additionalTemplateFilters: {
    message: input => {
      return `This is a much better string than "${input}", don't you think?`;
    }
  }
});

and within the template

    - id: publish
      name: Publishing to Code Source Repository - Github
      action: publish:github
      input:
        allowedHosts: ['github.com']
        description: This is ${{ parameters.component_id | message }}

Such a nice feature is not documented at all !!!

aureamunoz commented 3 months ago

Cool! Waou, this is not easy to understand from my side.

cmoulliard commented 3 months ago

Backstage template & scaffolding uses nunjucks under the hood to render the template.

It support the nunjucks syntax as described here: https://mozilla.github.io/nunjucks/templating.html#logic for files which are processed when we call the action fetch:template => example: what we do when we generate the java files, etc)

Unfortunately, nunjucks dont support a split function, this is what I discovered when I was using such a syntax could work parameters.quarkusVersion | split(':') | last | float

Hopefully, backstage allows to add your own filters using the parameter additionalTemplateFilters available on the function createRouter

Important: This option is not documented at all and not defined part of the schema and then non visible when we list the actions !

This test case should help you to understand a bit more: https://github.com/backstage/backstage/blob/e3c38284bfa723ea2956995e1cf07f02f4cf3665/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts#L106-L150

cmoulliard commented 3 months ago

Question: Should we design the filter as such to get a float object usign some nunjucks supported words + our own filter that we can compare to 3.9

      if: ${{ 'io.quarkus:quarkus-hibernate-orm-rest-data-panache' in parameters.extensions 
      and 
     (parameters.quarkusVersion | split(':') | last | float) <= '3.9' }}

OR should we aggregate the operations split(':') | last | float into one

      if: ${{ 'io.quarkus:quarkus-hibernate-orm-rest-data-panache' in parameters.extensions 
      and 
     (parameters.quarkusVersion | keyToFloat ) <= '3.9' }}

Note: keyToFloat could be renamed to something else toFloat, getVersionAsFloat or be extractKeyVersion | float etc

WDYT ? @aureamunoz @iocanel

aureamunoz commented 3 months ago

I don't have a very strong opinion on this. I would say that adding it to an existing filter if there is one that fits our use case, seems to me to be quicker to do

cmoulliard commented 3 months ago

We could test easily such additional filters =>

import {SecureTemplater} from './SecureTemplater';

function extractVersionFromKey(
  streamKey: string,
): string {
  if (!streamKey) {
    throw new Error(`StreamKey to be processed cannot be empty`);
  }

  let streamKeyArr = streamKey.split(":")
  if (streamKeyArr.length < 2) {
    throw new Error(`The streamKey is not formatted as: io.quarkus.platform:\<version\>`);
  } else {
    return streamKeyArr[1]
  }
}

describe('QuarkusStreamKey', () => {

  it('should get an error as stremKey is not well formatted', async () => {
    const renderWith = await SecureTemplater.loadRenderer({
      templateFilters: {
        extractVersionFromKey: (streamKey) => extractVersionFromKey(streamKey as string)
      },
    });

    let ctx = {inputValue: 'io.quarkus.platform-3.9'};
    expect(() => renderWith('${{  inputValue | extractVersionFromKey }}', ctx),).toThrow(
      /Error: The streamKey is not formatted as: io.quarkus.platform:<version>/,
    );
  });
});