blog.8-p.info

In this year, I’m going to reduce my usage of shell scripts in teams which my scripts would be maintained by someone else in the future. Yes. I know that what I type in zsh every day is considered as a shell script. I know the value of becoming a shell literate.

However, writing a shell script is difficult. And the difficulty is not worth dealing with.

Shell is a bad programming language

Let’s face it. Even with the unofficial bash strict mode, shell has too many pitfalls. You wouldn’t face it when you write a few lines of code and the script is running in your environment. But once the script is shared within teams, it would run under conditions that you haven’t considered, such as a file name with a whitespace character. That level of subtleness would easily break poorly-written shell scripts.

To be fair, shell has to be an interactive interface and a programming language. While most programming languages don’t have to be both, shell is trying to have the best of both worlds.

That being said, the attempt hasn’t been successful 1. You cannot have both. It is better to use a programming language when you need a programming language.

Recently Marc Brooker tweeted;

If sh-style shell programming was invented today, everybody would think it’s a sick joke.

And I do agree.

If you need a programming language, use an ordinal programming language.

You likely mix a few more languages

Average shell scripts tend to have awk, sed, and nowadays jq. We all have learned them somewhere in our careers. Combining these small tools shows the power of Unix.

However, it makes a barrier for people who don’t know them. Do they need to learn all of them to let computers do certain stuff and/or understand what I’ve written? I don’t think so.

Your environment might not have a shell

This would be weaker than the above two.

In environments like distroless container images, shell is one of the dependencies you need to explicitly install. Writing a shell script doesn’t reduce the number of dependencies, compared to other programming languages.

What would I do instead?

I would use Python or Ruby. I know Ruby better, but Python would fit better in Amazon Linux 2 or other distros that have Python by default. If I cannot have them, I might use Go or Rust.

Why don’t you use ShellCheck?

ShellCheck is good for pointint out pitfalls, but it is better to use a language with less pitfalls.

Isn’t it cumbersome to write Python/Ruby instead of writing shell scripts?

Hell yes! But writing a robust shell script is slightly, re-writing a huge shell script in Python/Ruby is more cumbersome for me.

On related note, Julia’s backtick syntax is a good progress regarding calling subprocesses safe and nice.

Running External Programs

The command is never run with a shell. Instead, Julia parses the command syntax directly, appropriately interpolating variables and splitting on words as the shell would, respecting shell quoting syntax.

How do you manage dependencies in Python/Ruby in this case?

No dependencies. I simply use “standard” packages which are the part of the language distribution.

It is also cumbersome. But even with just standard packages, for example, both Python and Ruby can do datetime calculation. You probably could do the same with GNU date (e.g. date --date '3 days ago') but then BSD date doesn’t support the syntax…


  1. Oil is an interesting attempt to make a shell-like, but saner programming language. ↩︎

This post is a bit unfinished, but I’d like to do what Julia Evans did on her Recurse Center series. Write what I know so far.

runc’s create command creates a container and then starts the container. This is a bit confusing if you are familiar about containerd where running containers are called “tasks”.

The actual code in create.go is quite simple.

	Action: func(context *cli.Context) error {
		if err := checkArgs(context, 1, exactArgs); err != nil {
			return err
		}
		if err := revisePidFile(context); err != nil {
			return err
		}
		spec, err := setupSpec(context)
		if err != nil {
			return err
		}
		status, err := startContainer(context, spec, CT_ACT_CREATE, nil)
		if err != nil {
			return err
		}
		// exit with the container's exit status so any external supervisor is
		// notified of the exit with the correct exit status.
		os.Exit(status)
		return nil
	},

All interesting things are happening in startContainer() which calls runner#run()

func (r *runner) run(config *specs.Process) (int, error) {
	var err error
	defer func() {
		if err != nil {
			r.destroy()
		}
	}()
...
	switch r.action {
	case CT_ACT_CREATE:
		err = r.container.Start(process)
	case CT_ACT_RESTORE:
		err = r.container.Restore(process, r.criuOpts)
	case CT_ACT_RUN:
		err = r.container.Run(process)
	default:
		panic("Unknown action")
	}

The type of r.container is libcontainer.Container. So let’s see Run() method there.

libcontainer is complicated

Run() calls Start(), which calls start().

func (c *linuxContainer) start(process *Process) error {
	parent, err := c.newParentProcess(process)
	if err != nil {
		return newSystemErrorWithCause(err, "creating new parent process")
	}
	parent.forwardChildLogs()
	if err := parent.start(); err != nil {
		return newSystemErrorWithCause(err, "starting container process")
	}
    ...

Hmm, what is the parent process here? I think that it may be related to runc’s init command, which has “do not call it outside of runc”.

var initCommand = cli.Command{
	Name:  "init",
	Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
	Action: func(context *cli.Context) error {
		factory, _ := libcontainer.New("")
		if err := factory.StartInitialization(); err != nil {
			// as the error is sent back to the parent there is no need to log
			// or write it to stderr because the parent process will handle this
			os.Exit(1)
		}
		panic("libcontainer: container init failed to exec")
	},
}

nsenter

Another thing I’d like to mention today is nsenter, a Go package inside libcontainer.

The nsenter package registers a special init constructor that is called before the Go runtime has a chance to boot. This provides us the ability to setns on existing namespaces and avoid the issues that the Go runtime has with multiple threads. This constructor will be called if this package is registered, imported, in your go application.

The special init constructor seems quite tricky.

Go has been the container language since Docker. Docker, containerd, runc and Kubernetes. All of them have been written in Go. But, sometimes I think that Go’s think runtime doesn’t fit well for low-level container stuff.

Update

Sam has tweeted:

I agree that Go’s runtime can be at odds with low-level syscall manipulation. For a while, there was a Rust OCI runtime from Oracle called Railcar (which seems to be dormant) and Red Hat has a C implementation called crun. But runc is far more widely used and has more scrutiny.

Yes. There are crun and railcar. Another “does Go really fit us?” moment was from amazon-ecs-shim-loggers-for-containerd

Note that golang has not included this fix in a specific version, and in order to take it effect, please build shim logger with go built from source.

I’m glad that the issue has been fixed in Go’s master at least. The fix will be included in Go 1.16.

I’m going to start a series that I read runc “cover-to-cover”.

runc is a small command which reads the OCI Runtime Specification and runs Linux containers. Docker, containerd or even CRI-O use runc under the hood by default.

But first, let’s talk about libcontainer

The part of runc is called libcontainer due to the origin. I’d like to cover the fact first since it is fascinating.

In 2014, Docker was attempting to extract its core as libcontainer.

Second, we are introducing a new built-in execution driver which is shipping alongside the LXC driver. This driver is based on libcontainer, a pure Go library which we developed to access the kernel’s container APIs directly, without any other dependencies.

While it didn’t thrive as a standalone project, libcontainer eventually became runc in 2015.

Docker has taken the entire contents of the libcontainer project, including [nsinit], and all modifications needed to make it run independently of Docker, and donated it to this effort. This codebase, called runC, can be found at github/opencontainers/runc. libcontainer will cease to operate as a separate project.

Because of that, a lot of low-level “guts” of runc is in libcontainer/ directory.

Where is the main function?

runc’s main function is in main.go. runc uses urfave/cli for handling its sub-commands and parsing the command line parameters.

func main() {
	app := cli.NewApp()
	app.Name = "runc"
	app.Usage = usage
...
	app.Commands = []cli.Command{
		checkpointCommand,
		createCommand,
		deleteCommand,
		eventsCommand,
		execCommand,
		initCommand,
		killCommand,
		listCommand,
		pauseCommand,
		psCommand,
		restoreCommand,
		resumeCommand,
		runCommand,
		specCommand,
		startCommand,
		stateCommand,
		updateCommand,
	}
...

All of the command functions are in separate Go files in the main package, such as checkpoint.go.

That’s it for today!

I’m going to read create.go this week.

I still read Twitter. Cindy Sridharan has started her new blog on Substack. Marc Brooker has shared his favorite papers in 2020. I want to know these information and Twitter is the only medium for me so far.

I also post this blog’s updates to Twitter automatically via IFTTT. I want to keep this blog static. So Twitter works as the lightweight comment/feedback system. Sometimes I tweet.

Yet, I want to reduce the time I spend on Twitter. Moreover I want to be more conscious about what I read in this year.

What I’ve tried

I’ve been using LeachBlock on my Firefox and limiting the time I spend on certain websites, but this strategy barely worked. Having 10 minutes of Twitter made me want to read, actually a bit more.

In hindsight, it’s an obvious outcome. Most of paid websites allow you to read a little without paying, because that makes you want to read a bit more. I made a system that would make that “craving”.

What I’m going to do

Instead, I’m going to use Twitter only on Saturdays. On other days, Twitter will be 100% blocked.

Hopefully the 6 days hiatus breaks my habit of checking Twitter aimlessly.

This is the 80th post of my #100DaysToOffload challenge. I would write the remaining 20 posts in 2021. Putting this blog on my back burner may make some space for other activities. Not so sure what would be “other activities” though.