Packaging an Application

In this guide, you will deploy a simple nginx web server, using the Package Operator - Package API.

During this guide you will:

  • Create a manifest.yaml file
  • Assign Kubernetes objects to PKO phases
  • Take advantage of templates
  • Setup test cases for templates
  • Build and Validate the Package Operator package

To complete this guide you will need:

  • A Kubernetes cluster with Package Operator installed
  • The kubectl-package CLI plugin
  • Access to a container registry where you can push your images

All files used during this guide are available in the package-operator/examples repository.

1. Start

To complete this step, refer to the files in /1_applications/1_start.

When packaging an application for Package Operator, you will need 2 things:

  1. One or more Kubernetes manifests (e.g. deployment.yaml)
  2. A PackageManifest object in a manifest.yaml file

Writing a PackageManifest

Like with any Kubernetes object, Group Version Kind contains the version information:

apiVersion: manifests.package-operator.run/v1alpha1
kind: PackageManifest

Metadata contains the name of this package:

metadata:
  name: nginx

Packages may be cluster-scoped, namespace-scoped, or both.
This controls whether you can install this package via Package or ClusterPackage API.
Namespace-scoped packages can not contain cluster-scoped objects, like Namespaces.

spec:
  scopes:
  - Namespaced

Phases are needed when you need a distinct order in your rollout or teardown.

Examples:

  • Ensure an application is completely upgraded before reconfiguring the load balancer
  • Run a database migration before bringing up the new Deployment
  • Ensure Custom Resource Definitins (CRDs) are Established before deploying your Operator
spec:
  phases:
  - name: deploy

Probes define how Package Operator interrogates objects under management for status.
PKO will only continue into the next Phase if all objects passed their availabilityProbe.

spec:
  availabilityProbes:
  - probes:
    - condition:
        type: Available
        status: "True"
    - fieldsEqual:
        fieldA: .status.updatedReplicas
        fieldB: .status.replicas
    selector:
      kind:
        group: apps
        kind: Deployment

Assigning objects to phases

Package Operator needs to know in which phase objects belong.
To assign an object to a phase, simply add an annotation:

metadata:
  annotations:
    package-operator.run/phase: deploy

Build & Validate

If you just want to validate the local package contents, run the kubectl package validate command:

kubectl package validate 1_applications/1_start


For example, if you encounter an issue such as a missing annotation, the output might look like this:

Error: Package validation errors:
- Missing package-operator.run/phase Annotation in deployment.yaml#0

To inspect the parsed hierarchy of your package, run the kubectl package tree command:

kubectl package tree 1_applications/1_start


Example output:

nginx
Package namespace/name
└── Phase deploy
    └── apps/v1, Kind=Deployment /nginx-deployment

Finally, build your package as a container image and push the image to a registry. To directly push images to a registry, assuming you’ve already logged in using podman/docker login, use the following command:

kubectl package build -t <your-image-url-goes-here> --push <path-to-package-contents>


Example:

kubectl package build -t quay.io/myquayusername/nginx --push 1_applications/1_start

Deploy

Now that you have built your first Package Operator package, we can deploy it!
You will find the Package-object template in the examples directory under 1_applications/package.yaml. Don’t forget to change the package url so it
corresponds to the one used when building the package. For example, replace image: <your-package-url-goes-here> with image: "quay.io/myquayusername/nginx".

If the package specifies a configuration set, its values are to be specified in
a config section within the spec. If a config entry has a default specified in
the package manifest it may be overridden here. If the package requires values
that are not defaulted and missing here, the package installation will fail.

Example Package object template:

apiVersion: package-operator.run/v1alpha1
kind: Package
metadata:
  name: my-nginx
spec:
  image: <your-package-url-goes-here>
  config:
    nginxImage: "nginx:1.23.3"
  1. To deploy the package, execute the following command:

    kubectl create -f 1_applications/package.yaml
    


    You will receive a confirmation message similar to:

    package.package-operator.run/my-nginx created
    
  2. Wait for package to be loaded and installed:

    kubectl get package -w
    


    The status will change from Progressing to Available:

    NAME       STATUS        AGE
    my-nginx   Progressing   11s
    my-nginx   Available     13s
    
  3. Finally, to verify the deployment status, run:

    kubectl get po
    

    A “READY” value of 1/1 and a “STATUS” of “Running” indicate a successful deployment.

2. Templates

To complete this step, refer to the files in /1_applications/2_templates.

Next, we want to make sure that the package can be installed multiple times into the same namespace. To accomplish this, we have to make the package more dynamic!

Go Templates

By renaming deployment.yaml to deployment.yaml.gotmpl, we can enable Go template support. Files suffixed with .gotmpl will be processed by the Go template engine before the YAML manifests are parsed.

TemplateContext is
documented as part of the API. It always contains information like package
metadata that can be used to reduce reduncancies.

app.kubernetes.io/instance: "{{.package.metadata.name}}"

Additionally, packages can include a config section. This section requires the config to be specified in the package manifest as an OpenAPI specification. It is recommended to require values that are always needed for package deployment and set defaults if appropriate.

To inspect the parsed hierarchy of your package when using a config section, you must provide a configuration file with the required values:

Example config.yaml:

nginxImage: "nginx:1.23.3"

Inspect the parsed hierarchy of your package:

kubectl package tree 1_applications/2_templates/ --config-path config.yaml

Alternatively, the config section of a named test template within the manifest
can be used:

kubectl package tree 1_applications/2_templates/ --config-testcase namespace-scope

Testing Templates

Using a template engine with yaml files can quickly lead to unexpected results.
To aid with testing, Package Operator includes a simple package testing framework.

Template tests may be configured as part of the PackageManifest, by specifying the TemplateContext data to test the template process with.

For each template test, Package Operator will auto-generate fixtures into a .test-fixtures folder when running kubectl package validate or build and compare the output of successive template operations against these fixtures.

Example of a template test defined in manifest.yaml:

test:
  template:
  - name: namespace-scope
    context:
      package:
        metadata:
          name: my-cool-name
          namespace: test-ns

Make a change to the deployment template:

#...
metadata:
  name: "{{.package.metadata.Name}}-deploy"
#...

Validate the package again:

$ kubectl package validate 1_applications/2_templates
Error: Package validation errors:
- Test "namespace-scope": File mismatch against fixture in deployment.yaml.gotmpl:
  --- FIXTURE/deployment.yaml
  +++ ACTUAL/deployment.yaml
  @@ -1,7 +1,7 @@
   apiVersion: apps/v1
   kind: Deployment
   metadata:
  -  name: "my-cool-name"
  +  name: "my-cool-name-deploy"
     labels:
       app.kubernetes.io/name: nginx
       app.kubernetes.io/instance: "my-cool-name"

To continue building the package, either reset your change to the template, edit the fixture or delete the .test-fixtures folder and regenerate all fixtures by running the validate command again.

Upgrade/Deploy

This section provides a steps on how to upgrade or deploy your package using the Package Operator.

  1. Build your package again using a different tag or image name:

    kubectl package build -t <your-image-url-goes-here> --push 1_applications/2_templates
    
  2. Edit the Package object on the cluster to change the image: to the new build tag. Example:

    kubectl edit package
    

    change:

    #...
    spec:
     config:
       nginxImage: nginx:1.23.3
     image: quay.io/myquayusername/nginx:0.0.1
     #...
    

    to:

    #...
    spec:
      config:
        nginxImage: nginx:1.23.3
      image: quay.io/myquayusername/nginx:0.0.2
    #...
    
  3. Monitor the change being rolled out:

    kubectl get package -w
    

    The status will transition from Progressing to Available.


The deployment’s name now utilizes the name of the Package object, making it safe to create multiple instances of the same package.

  1. Modify the name in 1_applications/package.yaml file and redeploy your package:

    • Change the Package name in the YAML file:

      #...
      kind: Package
      metadata:
        name: other-nginx
      #...
      
    • Redeploy the package:

      kubectl create -f 1_applications/package.yaml
      package.package-operator.run/other-nginx created
      
  2. Verify the changes:

    • Check the package status:

      $ kubectl get package
      NAME          STATUS      AGE
      my-nginx      Available   69m
      other-nginx   Available   6s
      
    • Check the deployments:

      $ kubectl get deploy
      NAME          READY   UP-TO-DATE   AVAILABLE   AGE
      my-nginx      1/1     1            1           6m25s
      other-nginx   1/1     1            1           117s