An Independent UI for Foreman and Plugins - Discussion and Investigation

Introduction

There has been a lot of discussion, both here on the community forum and outside of it, about re-architecting the UI for Foreman and plugins. Many ideas have been proposed, but these discussions tend to be theoretical and often branch into different directions. The goal of this post is to bring this topic for a larger discussion by identifying the problems with the existing UI, what should our goals be, and present the research I’ve done into this. Even after looking into it this week, there are still a lot of open questions and I would love to hear from other developers by opening up this discussion early.

Why?

First things first, why even change anything with the UI and it’s technologies?

  • Currently, development is difficult on UI pages. It tends to be slow and we haven’t made much progress in the past 2-3 years of introducing React and other new technologies into our stack. We have discussed re-writing pages and now do a better job working with UX professionals, to not be able to implement these changes at a relatively quick pace is holding us back.
  • Testing is difficult. With our UI stack being reliant on Rails, it is hard to do anything outside of unit/snapshot tests as you have to load the whole application to test anything from a functional perspective. This has made it hard to introduce testing technologies that help with e2e (or similar) testing, such as cypress.io, and use easy-to-configure CI services such as github actions. We do see UI bugs that seem like they could easily be caught by more comprehensive testing.
  • API: For Foreman core, the UI doesn’t completely rely on the REST API that is user-facing. This means that the external API doesn’t always have the functionality that the UI has and there is no way to guarantee this. Having the UI only communicate through the API would ensure the REST API has the same functionality as the UI. This would help hammer and other supported API client tooling as well.
  • Sharing and overwriting parts of the UI: We’ve struggled to work with plugins and overwrite parts of the UI. Deface is a technology that doesn’t exactly fix our needs. We’ve made improvements with this with slot+fill, but could perhaps even improve this further and come up with shared libraries and other ways to standardize the UI and plugin interaction.
  • Staying flexible: The UI (even the new parts) being reliant on Rails means that it’s hard to stay as flexible as we could in the UI. We are limited to the tools ruby/rails provides for JS integration. Sometimes this limits us or makes it more difficult to take advantage of the JS ecosystem. As one example, we use webpack-rails, a no-longer-maintained gem.

I would love to hear from others who work on the UI about any other pain points they have felt.

Goals

What are the goals of this investigation and discussion? I’d like to start with a “what do we want?” approach and not limit our thinking around the existing technologies. Here are my goals for the investigation and what I was trying to target with research:

  • The UI can be deployed and developed independently of the back-end: In general, decoupling the UI from the back-end. This would make it more testable, strictly enforce our API compatibility, and provide a better developer experience. This is not to say we couldn’t serve static assets through rails in a production environment, but my goal is that i can ‘npm start’ or ‘npm build’ (or an equivalent command) and have a fully functional and independent application that can pointed at a Foreman server.
  • Leverage our existing work: No one wants to “re-write the re-write”. This is a common criticism when the discussion of making changes in our UI technologies comes up and it is a valid one. We should use our existing work that moved pages and parts of the UI to React. IMHO Having some refactoring to fit a new model is fine, but if we need to do full re-writes of pages, that is not productive to us overall. Ideally, the newer React pages and components are moved over as-is to a new architecture.
  • Have any “new” UI pages work seamlessly with the existing UI: Let’s be very realistic here - some pages written in erb will likely live in our application for a long time. Going from an “old” page to a “new” page should feel and look the same to a user. This includes menus and authentication. We will want any new UI architecture to fit in comfortably with the old one. Let’s not assume that “this is temporary and we will move everything over soon”. Assume living with Rails-generated pages is here to stay for a while and any new technologies should co-exist with this, even if we have the goal of moving all the pages to a new UI model.

Challenging areas

So what is holding us back from moving to an independent UI model? I started to look into this because we have been having a lot of theoretical discussions, and it turns out many approaches fall apart when you try to implement something. Here are the areas that make us “unique” and it is important we acknowledge these first as a solution will have to solve these issues.

  • Plugins: This is challenging for a couple reasons
    • We allow for a variable amount of plugins to be installed - This means we have to accommodate many combinations of plugins and ensure the UI functions well.
    • Plugins can be installed after the initial installation - This means the UI has to be able to change in production. More on this later, but where we see an issue with this is any sort of compiled client-side assets. How would you change them when a plugin is introduced?
  • Menus and chroming: the menu and chroming such as the top bar will have to be the same as the existing pages.
  • Authentication: How does a user stay authenticated when they move to new UI pages?
  • Global state: How do we keep the information of what organization the user has selected and share that across plugins? (as one example)
  • Sharing components: How do we share components across Foreman and plugins?
  • Supporting disconnected installs: We support disconnected (no external network) installs, which means no reliance on hosted dependencies.
  • On-premise installation: We are not SaaS and many tools and workflows are designed around the hosted SaaS model. We need to account for this in our architecture and ensure the UI can be installed as we expect.

Investigation

There are different ways to approach this, but given the goals of an independent UI and the challenges of plugin architecture, it makes some of the more common models will be hard for us to implement.

The independent UI model makes me look at SPA (yes, I’ve somehow managed to avoid this word until here :wink:), meaning single-page-application, an application that handles routing internally and does not have to call back to the server to render every page. It turns out we rely on Rails engines quite a bit to handle a lot of plugin and dynamic logic for us, so there are a lot of challenges to moving the UI to be independent, especially with an SPA model.

The issue with this is having plugins, how do you extend an SPA? That made me look into managing multiple SPAs on the front-end. I was skeptical at first, but there are many benefits to this model and measures to make sure they co-exist in a non-bloated manageable manner. There are downsides as well that I will get into. This is what I spent the most time researching so here are my findings and I’ll go over some alternative approaches that I hope to research more as well:

Multiple SPA model

Disclaimer: This is not a proposal, just the research I’ve done into this approach that I am sharing, helping to identify the technical challenges of moving to an SPA model and patterns we may want to consider

This model is sometimes called ‘microfrontends’, which is annoyingly buzzwordy, but it really means serving multiple SPAs on the front-end and managing what shows where on the page and which routes display the SPA you want. There is a good write up on them here that goes into more detail. It also is an approach that the open-source Cloud Services for RHEL takes.

Using multiple SPAs on the front-end is appealing for a few reasons:

  • It fits our plugin architecture
  • Each team owns their own tech stack, meaning they can make their own decisions around testing, linting, CI, etc…
  • Reliance between applications is minified and limited. The shared parts of the application can be managed and independent applications will not rely on each other.

A popular library that is used for this is single-spa, which I used to test this concept out and get a kind-of-functional proof of concept here: GitHub - johnpmitsch/foreman-spa-microfrontend: A POC of building a Foreman microfrontend using https://single-spa.js.org/

I’m not pushing that we use the single-spa package if we go this direction, but it did let me test out the concept and identify some issues we would have using multiple SPAs.

The way single-spa works is it allows you to load an SPA on different parts of your application, on different routes and even within the same page. The SPAs can be written in different frameworks even, though I assumed we will all use React and standardize on a version.

Once I got it set up, I was able to create these applications:

  • root-config: the single-spa config that manages routing
  • foreman-chroming: This is an application that will always show and contain the menus and top bar (chroming) for the application. It will also be responsible for loading dependencies.
  • foreman-core: An application serving foreman core pages
  • katello: An application serving Katello pages.

These are all in the same repo to keep things easy, but in practice these would be separate repositories or repositories within a monorepo.

This worked well in my testing to always show chroming on the side and then load an application based on the route. You can try this out in the POC.

After that, I had some questions and tried to solve them best I can:

  • Will assets not double load if we are loading multiple React apps on a page?

    • Potential Solution: only load assets in foreman-chroming, which is assumed to be loaded on every page. This can be shared through webpack externals: example here
    • We could use the meta foreman-js packages and webpack externals to standardize the packages used for each application. The applications would have access to these package through webpack externals, so no common assets are double-loaded
  • How to dynamically load applications based on plugins being present?

    • We don’t have a pre-determined amount of plugins. How can we handle having the root-config use plugins as a dependency if we don’t know how many there will be?
    • Potential Solution: AFAICT, there isn’t support for having a dynamic amount of dependencies in your package.json file. So I used preinstall and postinstall scripts to edit the package.json file, add plugins based on a configurable file, and reset package.json back to the base state. I also have it set up environment variables that turn on when the plugin is present that could be used in the routing and application code. It does work well, but I’m open to other ideas :slight_smile:
  • How can plugins fill in other pages across applications?

    • This is a challenging one. Right now we allow plugins to fill in parts of the Foreman UI and I assume we want similar functionality.
    • I haven’t solved this in the POC, but here are my thoughts and potential solutions.
      • Can we adapt slot and fill to work in an independent UI model? This is a react-based approach that was added to foreman recently. Slot and fill is based on a global registry, so we would have to do something similar here, and that can cause more coupling between applications, which is discouraged. Perhaps the foreman-chroming application could be configured to support this as a source of truth for the slot and fill registry.
      • Could we have a shared foreman-components library that plugins can edit, gating functionality behind environment variables? With environment variables turned on for each application, we could have a shared library that loads parts of components dynamically. For example, it could have a HostForm component, and Katello can add functionality to this that is gated behind an environment variable, which is set during the pre-install npm script that I mentioned previously. It also allows these changes to be reviewed organization-wide, assuming that foreman-components is it’s own repo. This is a bit of a different model than we have now, but moving to a new UI architecture would allow us to approach extending functionality differently.
  • Menus: How do we use the same menu in both Rails pages and an independent UI?

    • Potential Solution: we could separate this menu into its own npm package that takes certain input. This would be used by Rails and the independent UI. This could be used to show the menu and we could use an API call to get the menu entries (this would probably need to be refactored on the back-end to do so). It also could be cached so the UI isn’t calling for the menu items every page load.
    • I haven’t fully fleshed out this idea so I am open to solutions: The criteria being the UI uses the same menu with Rails pages and new pages and we don’t duplicate code.
  • Routing: How do we route to both Rails and an independent UI application?

    • I would like to look into using Apache to do this. We can use it to route specific paths to the independent UI app and from there it is handled by single-spa routing or another application-based router. All other routes go to rails.
  • How do we install plugins after the initial installation?

    • This is unfortunately where this approach falls apart: An microfrontends app created in this manner bundles up assets with webpack and evaluates environment variables during compilation. So all logic is done client-side with static files, meaning no environment variables present with which you can base routing or what-shows-on-the-page.
    • This means we will likely need some sort of server-side logic if we want to install a plugin that adds new pages to the UI or extends parts of the UI. So I think compiling and serving static assets will be difficult, unless there is some sort of overarching server-side logic that manages them.

Please jump in if I am missing anything, but this approach did fall short when trying to figure out how plugins are installed after installation. However, there are parts to this approach I really like, and it highlights challenging areas that we will find in other approaches. Hopefully it was informative.

There are still a lot of other open questions such as authentication, browser history, versioning, deployment, development workflows, etc… However, I think the plugin model is going to be the hardest to solve and IMHO is the biggest hurdle to moving to an independent UI model. So that issue is where I’m starting my evaluation of different approaches.

Alternative approaches

As they say, there are many ways to skin a cat, and there are all sorts of UI architecture models out there. Here are a few that I briefly looked into and would like to research further. Please share any that you have encountered as well.

  • A server-side-rendered react application, separate from Rails, but using the server side rendering to dynamically handle plugins and other code. This can be based off configuration files that are updated by the installer. This would solve the “install additional plugins after initial installation” problem.
    • I know we are already rendering React server-side with Rails, but the point is it decouples our backend and frontend.
    • There could be potential in the microfrontend approach here or it could be a single SPA with certain points of extension, for example a directory you drop your plugin files into and the routing is based on a configuration file.
  • Apache SSI to handle server logic. I haven’t fleshed this out, but apache having a way to implement server-side logic could allow us to dynamically server static files based on a plugin’s presence.
  • Uber’s Fusion.js framework, a framework that supports React and apparently supports plugin architectures, allowing large applications to standardize parts and modularize parts of their application. I haven’t looked into it much, but it seems interesting and worth mentioning due to the plugin-able aspect.

API

A quick word on the API. To completely move to an independent UI, Foreman would have to have a fully functional external API. However, the current React pages are supported by the API, so the first step would be to move those over. That will be plenty of work even before we touch the API.

I bring this up because I see a lot of UI discussions go to a GraphQL vs REST api debate and that is not something we would have to solve immediately to create an independent UI, especially when talking about existing functionality. Unless I’m missing something obvious, let’s keep that conversation separate from this one :slight_smile:

Final thoughts

I would like to emphasize that I’m bringing this topic up for discussion in order to bring the conversations past the theoretical and actually investigate the technical hurdles. There has been no decision to change the Foreman UI and we are just exploring options. Perhaps moving to a fully independent UI won’t be for us, but I think it’s worthy of dicussion. Hopefully, developers find this useful and a good jumping off point for more discussion. :speech_balloon:

This was a huge brain dump so let me know if there are parts that aren’t clear or any questions. Thanks!

6 Likes

But all of this moves us away from merging Host and Content host in the UI. Are we giving up on this one?

I think there will be, sooner or later, need for integration of these SPA applications. Then we can easily end up with the same problem, just with more complex setup. And I am little bit worried about RAM usage and increased complexity of maintenance.

I wish we could take advantage of APIPIE metadata and simply generate a “hammer like” modern web user interface. It could be just “hammer on steroids”, with command and argument competition, fancy output with tables and links and stuff like that. A single SPA app generated from bindings. Plugins can easily contribute to API, add options etc. Problem solved :slight_smile:

Was there something in particular in the post that makes you think this? I would say a new architecture could make this more of a reality, for example if we go this direction:

We could have a shared foreman-components repository that is maintained by the whole Foreman organization, not just core, where we can keep Host table/details/forms and gate plugin functionality behind environment variables or some sort of flag that is set and evaluated. We can actually do this now and I would encourage it as it would make the components easier to move to a new UI format.

+1 These are valid concerns, we should consider them in any approach.

I’m a bit wary of a one-size-fits-all approach but perhaps there is some way we can use apipie to our advantage with a new approach to the UI

I think I did not understand fully how this would work. I thought in this design there is nothing shared, like completely decoupled apps with own stacks. This looks like a hybrid, that would technically work.

It did take me a bit to figure it out as well, It is decoupled apps with their own stacks, though you can share and collaborate through a separate library like in the example of foreman-components. I just haven’t figured out how to modify any gated functionality after it has been compiled by webpack, which is the current blocking issue to this approach. We would have to do this while installing a plugin after the initial installation.

There are other ways to communicate between apps such as observables and shared state, though these are somewhat discouraged in a multiple SPA setup, especially shared state. I could see observables being useful to us for organization selection though.

Thanks for the detailed and thorough investigation. You are confirming what I’ve always felt: the Javascript ecosystem usually assumes a very tightly controlled environment where runtime plugins aren’t really a thing where for us it’s the core deployment model.

I don’t want to comment too much on specifics because I gave up on front end development.

I think we should not accept this notion. Having to support both is a major pain. Most rails views in Foreman are actually quite simple: just a table listing with pagination or simple forms. If this is hard to do with pure Javascript, then I’d question if we’re doing it right. We should optimize tooling to make this easy since it’s the majority of our pages.

Something I haven’t seen is an investigation into webcomponents. Our application currently has very little state in the UI. Can’t we share the modern UI components via webcomponents, allowing ERB generated pages to look and feel the same as the Javascript UI?

This implies a NodeJS server. That means there’s another component for server admins to maintain, that can fail and needs to be kept up to date with security fixes. I’d like to avoid this.

I would like to avoid this. Configuring Apache can be complicated and right now development environments don’t require this, except Katello. Experience tells us that the Katello development environment is a pain to keep up and breaks all the time. It also means we need to rebuild the entire CI to use containers because we need to deploy an Apache instance for every setup.

Right now we provide a context to the base HTML. Would it be possible to serve that as a JSON endpoint and expand it with the loaded plugins? The Javascript application can then initialize itself by loading additional code. This keeps it much more straight forward and easier to test.

1 Like

I’m wondering if we actually need to support runtime plugins. I believe both commercial distributions have a static plugin set. And with a containerised deployment we also have a hard time to support runtime plugins.
Can we provide a page/script/tool where users select the plugins they want to use and then build and ship the app? We need plugin support, but I believe runtime support is nothing we really need.
If we find a workaround for this, would it make things easier?

1 Like

My thinking is that historically any transitions to a new architecture, or even just moving code as-is, takes a lot of time and we often will do something under the guise of it being “temporary” and we will live with that temporary fix in perpetuity.

Assuming we continue using React for a new UI architecture, we still have a lot of pages to rewrite and I foresee it taking a long time and being done page-by-page, that is even assuming we are able to find time to rewrite them all. IMHO assuming the “new” and “old” will co-exist would be the way to approach this.

native webcomponents could be worth looking into

I think we shouldn’t rule this out, but I understand the hesitation. We do have nodejs installed already and if we are able to use a lightweight application to handle routing and dynamically loading static assets it could be worth evaluating.

I would have to understand this a bit better, but I do think there could be a solution that involves Rails serving up the html and a javascript application initializing itself on top of that. I haven’t gotten farther than a brainstorm though.

It definitely would make things easier to have a static set of plugins. But I know one common complaint of Katello is that it can’t be installed on top of Foreman, so I imagine people would feel the same about other plugins. Also, we would have to reconsider our use of the word “plugin” if we moved to this model, to me that means you can install functionality after the initial installation.

There may be a way to incorporate webpack in the installer and have it regenerate static content as plugins are added. The idea of webpack in our installer scares me :fearful: but I haven’t actually thought it through completely, maybe it can be done in a maintainable way.

Another thought I had is to ship all plugin’s compiled front-end code and then it is somehow (not sure how yet) enabled to be served as plugins are added. This could have its benefits but would also require all plugins to be open-source and added to a registry or monorepo somewhere. I don’t know if anyone out there has internal foreman plugins, if so they would not be supported with this model.

I read your post as if it was a long term solution. I agree we’ll need a transition period but think it’s important to have the clear goal to fully transition. Completing transitions in the UI is something we’ve historically have had a hard time with.

No, nodejs is a development and a build time dependency. For RPM based installations no nodejs packages are installed on a production installation.

A similar solution is the Apipie indexes we have. Those are regenerated on the actual machines since they’re composing installed plugins, but are served as static files. AFAIK they can also be served as dynamically generated pages.

Yes, this is common in the community. IMHO if we require all plugins to live in the same repository, we might as well drop the whole concept of plugins and move to feature flags.

gotcha, good to confirm and I agree with that sentiment. My understsanding is: to keep installing plugins at runtime, we’ll need to have a way to modify the code or load additional files that reach client-side.

Ah thanks, that does make sense now that I think about it. Its just hard for me to imagine an independent front-end without having some sort of logic on the backend because of our plugin architecture. This is where I kind of go in circles.

My point is I would like to not rule anything out or discourage any approaches at this conceptual level, but I get being concerned about new dependencies and yet-another-service and appreciate it being brought up. It is hard to take the conversation further without POCs or a detailed design, which we don’t have yet for any sort of server-side-rendering.

I am glad you ask, because I question this every single day and I did not want to be the one asking this as a red hatter.

I fully understand that open core design is something that helps to grow, enables to integrate and makes software very flexible. But it comes with price, and this can be huge. We all know how much we need to pay, development is complex, UI is almost impossible to solve in a user-comfortable way, installation is hard (katello or not katello), we see confused users (which plugin supply what feature) and developers (where do I start), packaging is a huge effort, many code changes go into multiple repositories, overall it’s challenging.

Foreman GH org as of today has 176 git repositories and I believe we passed the growh stage at this point, Foreman is stable enough and we barely do any radical changes in its competencies or core features. I think we can afford some changes in this regard now.

This challenge can be reasonable decreased. The solution would be something we discussed in the past several times - merging plugins into core. Of course, we don’t want to simply give up our plugin API we have today, I think we should keep it alive and advertise it as an incubator for ideas and as an API for users and 3rd parties to integrate. We would disencourage from building user interfaces for plugins tho, we should encourage users to build a separate web applications and API (either GraphQL or REST).

We have even identified which plugins are good candidates, but looks like we are not sticking with the plan or not implementing it fast enough. I think we need to say loud that the ultimate goal would be to merge Katello at some point, the biggest plugin, defining the Foreman of the future.

This must go hand in hand with puppet changes @tbrisker 's team is working on and maybe it could help to answer the question - to make puppet a plugin or make a good enough API to be able to turn some features off. Because if we have many features in Foreman, such a thing would be quite useful. Especially if we are able to install with Katello but user could turn some features off (e.g. Candlepin - subscription management). With puppet installer, it should not be that hard to install that feature later on.

What that on the table, I think we should rewrite the UI from scratch, on a single JS stack. For once, and forever.

1 Like

As I’m one of the many that have complained about this, I do think we should still offer this functionality. The question is what steps need to be taken to achieve this.
In an ideal word, an admin would just type yum install katello and that’s it. But the admin could also type foreman-builder --add-plugin katello, this would build a new container with the katello plugin added. If this takes katellos js sources, adds them to some kind of config file and runs webpack or adds a new line to a bundler.d/katello.rb does not matter for a user. That’s what I mean: It does not have to be a runtime plugin.

I can relate to that, but I believe it might be the best option. Let’s not rule this out.
We’re currently designing a plugin API for lisa. If we have something to share, I’ll post it here.

FWIW, we have internal plugins.

That’s basically the same model I’m suggesting. Let’s take a bunch of plugin code and compile the whole set.

I think one reason that makes this difficult is webpack. Having to compile in a format that explicitly isolates parts makes it challenging.

On our Ruby side we can deal with this because it’s not compiled and we can reach out to other parts. There are integration points with a shared plugin loader.

IMHO the problem is webpack and how we deploy it. Can’t we create statically shipped files for each plugin + core and integrate them in the browser using a shared plugin loader similar to how we do it on the Ruby side? I believe this is what @John_Mitsch mentioned with referred to with his enhanced slot and fill proposal.

I don’t have a definitive answer to this, but I can say that Satellite has shipped Kubevirt as an optional plugin because it was a tech preview.

This has come to my mind as well, and definitely comes to light when looking into making significant technological changes to our stack. Having plugins tends to make us a special use case in any technology and I know we do all sorts of workarounds to accommodate a plugin infrastructure, especially on the front-end. Its hard to use certain technologies according to their best practices.

I think this should be entertained, and I like the idea of limiting the UI for plugins. After all, you can make your own SPA that sits on top of the API and your plugins functionality. I do feel like we are in need of simplification, especially around repo sprawl, and a lot of our core problems stem from the amount of packages and repos we have to juggle. Some of the recent nightly issues had to do with managing hammer plugin package versions, just one example of many. It’s not very realistic for us to expect to manage these in a sustainable way. We should at least simplify things for ourselves.

@lzap while there is some overlap with the UI topic, changing the plugin infrastructure does seem like it’s own discussion and IMO would be a good candidate for a new thread. I would hate for this thread to get too large in scope and everything to get lost in the noise as these are great points being made :slight_smile:

It does seem like a approach that would keep the front-end files static and independent but allow them to change according to plugins. I’ll have to look into it more and see if I can extend the POC to do this, perhaps just through a simple script to start. Please do share anything from your end, thank you.

Could we go one step further? The problem might be how we deploy our app and not webpack itself. Let’s assume for a second, that webpack was great and we’d just use it wrong because we try to use the same techniques that work for ruby for the javascript world?
To clarify: I think we have to do the plugging before compiling anything (e.g. before running webpack) and we should be better off than what we do now (run webpack for every plugin independantly and mix
the compiled files together).

We just merged a PR for lisa that introduces a plugin-architecture using a slot and fill mechanism. The repo contains a sample plugin. Please do check it out if you’re interested in how we did this.

3 Likes