rubydockerrspecrubygemsserverspec

Checking on empty gems cache fails using Serverspec for Docker image build testing


I'm currently having an issue with testing Docker image builds by means of Serverspec. In a nutshell, what I want to do is ensuring that during the image build the Ruby gems build cache gets cleared explicitly, e.g. by issuing rm -rf /usr/lib/ruby/gems/*/cache/*.gem in the Dockerfile.

The Dockerfile skeleton I'm working with looks like this:

 # Dockerfile

 FROM alpine:3.7

 RUN apk add --no-cache \ 
     dumb-init \
     ruby \
  && apk add --no-cache --virtual .build-deps \
     build-base \
     ruby-dev

 RUN gem install --no-rdoc --no-ri json \
  && gem install --no-rdoc --no-ri oj

 RUN apk del .build-deps \
  && rm -rf /var/cache/apk/* \ 
     /tmp/* /var/tmp/*

Before adding the gem cache removal command, I implemented the following Serverspec test to be able to start from a failing test:

 # ./spec/Dockerfile_spec.rb

 describe "Dockerfile" do
   before(:all) do
     @image = Docker::Image.build_from_dir('.')
     @image.tag(repo: 'demo', tag: 'latest')

     set :os, family: :alpine
     set :backend, :docker
     set :docker_image, @image.id
   end


   it "removes build dependencies during cleanup" do
     # Some more assertions 
     # ...
     expect(Dir.glob('/usr/lib/ruby/gems/*/cache/*.gem').size).to eq 0  
   end
 end

Assuming the Dockerfile from above, the tests run green:

 $ bundle exec rspec spec/Dockerfile_spec.rb
 ....
 Finished in 3.95 seconds (files took 0.24091 seconds to load)
 4 examples, 0 failures

However, this should not be the case since the gem cache has not yet been cleared and thus is not empty, i.e. I'd expect the corresponding assertion to fail during test execution.

This can easily be verified by launching a container from the freshly built image and checking the gems cache directory:

 $ docker run --rm -it demo:latest sh
 / # ls /usr/lib/ruby/gems/2.4.0/cache/ 
 json-2.1.0.gem  oj-3.4.0.gem

Testing the other way around and expecting a non-empty directory, the execution fails with an error message:

 # ./spec/Dockerfile.rb

 it "removes build dependencies during cleanup" do
   # Some more assertions 
   # ...
   expect(Dir.glob('/usr/lib/ruby/gems/*/cache/*.gem').size).to_not eq 0  
 end


 # Command line 

 $ bundle exec rspec spec/Dockerfile_spec.rb
 .F..

 Failures:

   1) Dockerfile removes build dependencies during cleanup
      Failure/Error: expect(Dir.glob('/usr/lib/ruby/gems/*/cache/*.gem').size).to_not eq 0

      expected: value != 0
           got: 0

      (compared using ==)

So obviously, the Dir.glob() command does not return the correct number of files in the gems cache directory.

Funny enough, running the Dir.glob() command manually inside the container returns the expected result:

 $ docker run --rm -it demo:latest sh
 / # apk add --no-cache ruby-irb
 ...
 / # irb
 irb(main):001:0> Dir.glob('/usr/lib/ruby/gems/*/cache/*.gem').size
 => 2

First, this made me think that the test is not properly executed within the container but on the host instead, but further experiments could not confirm this.

You have any ideas? Could this be a Serverspec/Rspec issue?

Thx!

Edit

First, this made me think that the test is not properly executed within the container but on the host instead, but further experiments could not confirm this.

I finally found out that this assumption was wrong. Actually, the Dir.glob() calls are executed outside the container, for whatever reason.


Solution

  • I finally figured out what went wrong, and actually it's pretty simple:

    Let's assume that we have a single Serverspec test case like this one:

    it "removes build dependencies during cleanup" do
      # Some more assertions 
      # ...
      expect(Dir.glob('/usr/lib/ruby/gems/*/cache/*.gem').size).to_not eq 0  
    end
    

    Now what's happening during execution is that the Dir.glob() call is evaluated before the expect() routine is invoked. Consequently, as I already pointed out in my post, the Dir.glob() command is run outside of the container's scope and checks the corresponding host directory instead. If you ever want to check if a container folder is empty or not, you can achieve this as follows:

    it "clears gem/apk caches as well as tmp files/dirs" do
      expect(command('ls /usr/lib/ruby/gems/*/cache/*.gem | wc -l').stdout).to eq "0\n"
    end
    

    Admittedly, this is probably not the most beautiful and elegant solution but basically does the job. If you have other ideas and proposals, feel free to post them.