Multi-Component Packages

Multi-component packages are useful when deploying larger and more complex projects via Package Operator.

They enable users to split the application into multiple components, each one structured as a standard Package Operator package, and ship them within the same package image.

Having a single image keeps the build phase simple, while, at the same time, separating components into their own packages gives the project authors several advantages:

  • component packages may be reused by other applications
  • package configuration is isolated from each other
  • every package can be rolled back individually

Structure

Example structure of a multi-component package

my-application
├── components
│   ├── backend
│   │   ├── deployment.yaml.gotmpl
│   │   └── manifest.yaml
│   └── frontend
│       ├── deployment.yaml.gotmpl
│       └── manifest.yaml
├── backend-package.yaml.gotmpl
├── frontend-package.yaml.gotmpl
├── manifest.yaml
└── namespace.template.yaml.gotmpl

A multi component package starts from the same structure of a simple package: a set of resources and a manifest.yaml file in its root directory. This is called the root component

The components folder is the other important piece: it is not part of the root component, instead it contains one subfolder for each component belonging to the package.

The structure inside each component subfolder matches the one of a standard Package Operator package: a manifest.yaml file with its own resources.

Important notes

  • Only a single level of components is allowed, to limit the complexity of packages (e.g. components/backend/components would not work in the example above).
  • Each component, including the root, has no awareness of the other components’ resources or configuration.

Enable the feature

Package authors have to opt into using multi-component packages by setting the .spec.component property in the root PackageManifest, to prevent breaking Packages already shipping a /components folder. The empty object may be used in the future to configure the multi-component feature.

apiVersion: manifests.package-operator.run/v1alpha1
kind: PackageManifest
metadata:
 name: my-application
spec:
 components: {}

Build

The validate and build phase is exactly the same as a simple package (tl;dr kubectl package validate and kubectl package build).

It is possible to validate a single component independently running kubectl package validate components/<component_folder>

Note on the tree command

The kubectl package tree, if called from the root directory, will show the structure of the root component only. To see the structure of each component, a separated call must be made indicating the component subfolder. Here are some examples considering the structure shown above:

# show the root component
$ kubectl package tree my-application
my-application
Package app-ns/app
└── Phase deploy-backend
│   ├── package-operator.run/v1alpha1, Kind=Package /app-backend
└── Phase deploy-frontend
    └── package-operator.run/v1alpha1, Kind=Package /app-frontend

# show the backend component
$ kubectl package tree my-application/components/backend
my-application-backend
Package app-ns/app
└── Phase deploy
    └── apps/v1, Kind=Deployment /app

# show the frontend component
$ kubectl package tree my-application/components/frontend
my-application-frontend
Package app-ns/app
└── Phase deploy
    └── apps/v1, Kind=Deployment /app

Note on the update command

The automatic image digest resolution feature can be used in multi-component package independently in each component.

Like the tree command described above, the kubectl package update command will consider only the selected component (the root one if called with the root folder path), read its manifest.yaml file and produce a manifest.lock yaml file.

# this will read my-application/manifest.yaml
# and generate my-application/manifest.lock.yaml
$ kubectl package update my-application

# this will read my-application/components/backend/manifest.yaml
# and generate my-application/components/backend/manifest.lock.yaml
$ kubectl package update my-application/components/backend

# this will read my-application/components/frontend/manifest.yaml
# and generate my-application/components/frontend/manifest.lock.yaml
$ kubectl package update my-application/components/frontend

Deploy

Each component of a multi-component package can be deployed, like any other simple package, as Package or ClusterPackage depending on the scopes defined in its PackageManifest.

The .spec.component field in Package (or ClusterPackage) determines which component will be installed from the specified image.

  • If missing or empty, the root component will be installed
  • If non-empty, its value must match the name of the subfolder containing the desired component
# deploy the root component of my-application
apiVersion: package-operator.run/v1alpha1
kind: Package
metadata:
 name: my-application
spec:
 image: quay.io/my-application
# deploy the backend component of my-application
apiVersion: package-operator.run/v1alpha1
kind: Package
metadata:
 name: my-application
spec:
 image: quay.io/my-application
 component: backend

If the .spec.component value doesn’t match any of the components bundled in the image, the package deployment will fail with a related error message.

IMPORTANT: since each component is in fact an independent package, each Package or ClusterPackage will deploy only one of them within a single definition. Read the General Tips section to understand how to setup more complex deployments.

General Tips

From a technical perspective, each component, including the root, is completely unrelated with the others and could contain any kind of resources.

However, if the multi-component package contains a set of parts that are intended to work together (like backend and frontend in our example above), the root component could be used to define the standard layout in which those components are supposed to be deployed together.

In this way a package user can deploy the single application in the “quick-way” by deploying the root component and let it take care of wiring the pieces together, or just one specific part by relying on its component.

This is achieved by placing Package or ClusterPackage definitions of each component inside the root component (plus any other useful definition, like the Namespace in the example above). This will leverage an already existing capability of Package Operator.

The root component in the example above, in fact, contains backend-package yaml.gotmpl and frontend-package.yaml.gotmpl resources, which are designed to deploy the two components as independent Packages (which will be owned by the main one) and configure them to work together.

The templating in this case allows to specify a general root component configuration which will be injected in the configuration of each component package accordingly.