Skip to content

Instantly share code, notes, and snippets.

@blaisep
Last active July 18, 2023 01:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blaisep/0b8ddf61dcbf6937a1a6c0c2c394c4d3 to your computer and use it in GitHub Desktop.
Save blaisep/0b8ddf61dcbf6937a1a6c0c2c394c4d3 to your computer and use it in GitHub Desktop.
Outline for an exercise in migrating a shell command to pypyr

Beyond shell scripts

Background: The objective is to run python everywhere and my first step is to replace shell activity with pypyr. These notes relate to the process of automating a common task: running a containerized application in a local virtual host. It happens to the podman, but the structure is similar to Docker, Vagrant, kubectl, etc. The naive workkflow of "copy/paste commands into yaml blocks" resulted in the question of how to handle common failure scenarios. So I wonder if there is an approach or even some scaffolding for the "typical launch script"

Starting a service requiring a Docker host

What things could go wrong?

The happy path is usually what is left over after everything else has failed. My intuition is that one should probe for the common failure conditions first. I didn't do that and so I ran into the failures organically, instead of seeking them out.

Is the service already running?

The app runs in a container and it requires a running podman virtual machine. This is structurally similar to docker, vagrant, etc.

In the shell, I would run:

 podman machine start                                                                                                                      ──(Sun,Sep18)─┘
Starting machine "podman-machine-default"
Waiting for VM ...
Mounting volume... /Users/bpabon:/Users/bpabon

This machine is currently configured in rootless mode. If your containers
require root permissions (e.g. ports < 1024), or if you run into compatibility
issues with non-podman clients, you can switch using the following command:

	podman machine set --rootful

API forwarding listening on: /var/run/docker.sock
Docker API clients default to this address. You do not need to set DOCKER_HOST.

Machine "podman-machine-default" started successfully

So, I suppose I could tell pypyr to listen for the string started successfully or I could give podman a flag to return a POSIX result code.

This simple pipeline so far assumes that it must start the host VM and also the application container.

steps:  
	- name: pypyr.steps.cmd  
		description: starts the podman machine but doesn't run it in a shell.  
		in:  
			cmd: podman machine start  
  	- name: pypyr.steps.cmd
    		description: --> giving pypi 10s to start the machine.
    		in:
      			cmd: sleep 10
	- name: pypyr.steps.debug  
		description: check out the cmd output saved to cmdOut!  
		in:  
			debug:  
			keys: cmdOut  
	- name: pypyr.steps.echo  
		run: !py cmdOut.returncode == 0  
		in:  
			echoMe: "Machine podman-machine-default started successfully: {cmdOut.stdout}"
	- name: pypyr.steps.cmd  
		in:  
			cmd: >  
				'podman run  
				--name api-server   
				--detach  
				--tty  
				--volume ~/.ara/server:/opt/ara  
				-p 8000:8000  
				docker.io/recordsansible/ara-api:latest'

Is the launch not yet complete?

If the VM is already starting, the next steps in the pipeline will fail. - I should check the state of the VM first. If I try too quickly, I get:

Error: cannot start VM podman-machine-default: VM already running or starting
Error while running step pypyr.steps.cmd at pipeline yaml line: 2, col: 5
Something went wrong. Will now try to run on_failure.

CalledProcessError: Command '['podman', 'machine', 'start']' returned non-zero exit status 125.

Error: cannot start VM podman-machine-default: VM already running or starting
Error while running step pypyr.steps.cmd at pipeline yaml line: 2, col: 5
Something went wrong. Will now try to run on_failure.

CalledProcessError: Command '['podman', 'machine', 'start']' returned non-zero exit status 125.

Idempotency: Did we already launch the service?

The host service may already be running from a previous invocation, so we should proceed to the application stage.

Error: cannot start VM podman-machine-default: VM already running or starting

Solution: retry until ready

use retry to keep on retrying a command until it's successful. For example:

- name: pypyr.steps.cmd
	description: --> installing just published release from pypi for smoke-test
	retry:
		max: 5
		sleep: 10
	in:
		cmd: pip install --upgrade --no-cache-dir {package_name}=={expected_version}

A complex command with diff types of arguments

From the shell prompt, I start the application calling the podman CLI, followed by the run command, followed by several options. Some of these options have additional parameters:

 podman run --name api-server --detach --tty \
  --volume ~/.ara/server:/opt/ara -p 8000:8000 \
  docker.io/recordsansible/ara-api:latest

Name collisions?

The application container may collide with another if the names are the same. Most often, this is because I have run the command twice.

Error: error creating container storage: the container name "api-server" is already in use by a8673f400c138e96cd9e036bfa47bd8285d058953635b614511723ba0d8bed7d. You have to remove that container to be able to reuse that name: that name is already in use

Solution: pipeline-reserved names

Consider using a known name - let's say api-server__, and then making it so that only your pipelines are supposed to use that name.

A similar approach can work with network ports if another container is using the same number.

Solution: foldable literal blocks

Yaml has the > symbol to indicate a foldable quote. Each line is interpreted with new lines rendered as spaces:

  - name: pypyr.steps.cmd  
		in:  
		cmd: >  
			podman run  
			--name api-server__   
			--detach  
			--tty  
			--volume ~/.ara/server:/opt/ara  
			-p 8000:8000  
			docker.io/recordsansible/ara-api:latest

Anatomy of a pipeline for launching a containerized app

Make sure as part of your pipeline that you:
a) check the it's not already running. if it is. . . stop it and start a new instance
b) do your work
c) clean-up after fail (stop the instance).

steps:
  - name: pypyr.steps.call
    comment: Block of pre-flight tasks - comments don't get exposed at runtime.
    in:
      call: start-services

  - name: pypyr.steps.echo
    description: Running the steps specific to this pipeline
    comment: Block of tasks to meet the objective
    in:
      echoMe: do your work here and in subsequent steps once services started

  - name: pypyr.steps.call
    comment: stop the services when done. could also put this in `on_success` group.
    in:
      call: stop-services

# This block is called in the beginning.
start-services:
  - name: pypyr.steps.cmd
    comment: this cmd should return 0 if podman api-server already running
    swallow: True
    in:
      cmd: echo some sort of cmd to check if api-server is running

  - name: pypyr.steps.call
    comment: if previous step errored, means podman already running.
             so stop it first, so we can start clean.
    run: !py "'runErrors' in locals()"
    in:
      call: stop-services

  # if stop is asynchronous/detached, you might have to have another retry-style
  # step here to wait for it to stop completely.
  
  - name: pypyr.steps.cmd
    comment: now we know api-server is deffo NOT running. so can just start it here.
    in:
      cmd: echo start your api-server 

  - name: pypyr.steps.cmd
    comment: this will retry max 5 times w 10s sleep in between to wait for api-server to start
             if api-server does not start in this time, will raise error and stop here.
    retry:
      max: 5
      sleep: 10
    in:
      cmd: echo some sort of cmd that returns 0 when api-server started and ready

# this block is called at the end.
stop-services:
  - name: pypyr.steps.cmd
    in:
      cmd: echo podman stop api-server__ stop etc.

# this is the global err handler. this on_failure group will run if any unhandled
# (i.e un-swallowed) error in the pipeline happens.
on_failure:
  - name: pypyr.steps.call
    comment: try to stop the services. this is best effort, so if this fails
             just swallow, not much more we can do.
    swallow: True
    in:
      call: stop-services

# The destination of the happy path
on_success:
  - name: pypyr.steps.echo
    description: Joyous, we embark on the happy path
    comment: announce helpful info, consider some variable substitution
    in:
      echoMe: You are ready to use at URL... reports at URL:// ...
      

How to clean up after the pipeline.

It's good to anticipate these conditions, it's even better to avoid creating them. Try to leave things in a good place for the next run.


Colophon

Tech docs are often considered to have two dimensions, Understanding & Action, each with a spectrum from Abstract to concrete.

The current Pyper docs are heavier on understanding (explanation and reference) and lighter on actions (tutorials and guides). I would be curious about developing a process for selecting scenarios and describing their structure.

@blaisep
Copy link
Author

blaisep commented Sep 20, 2022

@yaythomas , I tidied this up, I think the next step would be to adjust the example code to make it more literal (and distinct from the general "anatomical" example at the end.)
LMK if you have other editorial suggestions and I'll work them into a pull request to the docs repo.

@yaythomas
Copy link

Firstly, this would make a great blog article if you happen to be writing on dev.to or medium or something like that, well done @blaisep!

Secondly: some editorial notes re inclusion at pypyr.io docs website. The bare bones are absolutely there, but I would like to keep the docs website more targeted with precise and working "copy-able" reference examples. So as you say, make it more literal ("concrete" in chess terminology), rather than the rough bare-bones pipeline with echos all over the place.

So I'm thinking of adding a menu item on pypyr.io for tutorials or walkthroughs or something like that (or maybe under Pipelines> there can be an examples> or guides> or something of the sort - still thinking it through to see what would taxonomically make most sense). And then this Guide of yours would be the first entry!

But for it to be a Guide it would be nice if the example yaml is less exploratory and more definitive, showing the actual/exact podman commands - so that by the end we can see a working pipeline achieving the objective you lay out in para 1.

BTW: Another option if you want to wait a bit before doing something is to use the OS's sleep - e.g if after a detached command runs you want to pause before entering the retry loop checking service availability.

  - name: pypyr.steps.cmd
    description: --> giving pypi 10s before testing release
    in:
      cmd: sleep 10

@blaisep
Copy link
Author

blaisep commented Jul 17, 2023

Hi @yaythomas , I think I will consider the dev.to suggestion.

I have also considered a series of exercises like "katas", where I refactor a shell script into a better tool, like pypyr.
I found this handy shell script:
https://gist.github.com/blaisep/0f7d4aaa0361c8a3c646fa89f9bccc98
which is great, except that is is a shell script. So I will make it into a pypr pipeline(s).

@yaythomas
Copy link

nice, nifty idea!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment