# This class represents a Docker Image. class Docker::Image include Docker::Base # Given a command and optional list of streams to attach to, run a command on # an Image. This will not modify the Image, but rather create a new Container # to run the Image. If the image has an embedded config, no command is # necessary, but it will fail with 500 if no config is saved with the image def run(cmd=nil) opts = { 'Image' => self.id } opts["Cmd"] = cmd.is_a?(String) ? cmd.split(/\s+/) : cmd begin Docker::Container.create(opts, connection) .tap(&:start!) rescue ServerError => ex if cmd raise ex else raise ServerError, "No command specified." end end end # Push the Image to the Docker registry. def push(creds = nil, options = {}) repo_tag = info['RepoTags'].first raise ArgumentError "Image is untagged" if repo_tag.nil? repo, tag = Docker::Util.parse_repo_tag(repo_tag) raise ArgumentError, "Image does not have a name to push." if repo.nil? credentials = creds || Docker.creds || {} headers = Docker::Util.build_auth_header(credentials) opts = {:tag => tag}.merge(options) connection.post("/images/#{repo}/push", opts, :headers => headers) self end # Tag the Image. def tag(opts = {}) self.info['RepoTags'] ||= [] connection.post(path_for(:tag), opts) repo = opts['repo'] || opts[:repo] tag = opts['tag'] || opts[:tag] || 'latest' self.info['RepoTags'] << "#{repo}:#{tag}" end # Given a path of a local file and the path it should be inserted, creates # a new Image that has that file. def insert_local(opts = {}) local_paths = opts.delete('localPath') output_path = opts.delete('outputPath') local_paths = [ local_paths ] unless local_paths.is_a?(Array) file_hash = Docker::Util.file_hash_from_paths(local_paths) file_hash['Dockerfile'] = dockerfile_for(file_hash, output_path) tar = Docker::Util.create_tar(file_hash) body = connection.post('/build', opts, :body => tar) self.class.send(:new, connection, 'id' => Docker::Util.extract_id(body)) end # Remove the Image from the server. def remove(opts = {}) connection.delete("/images/#{self.id}", opts) end alias_method :delete, :remove # Return a String representation of the Image. def to_s "Docker::Image { :id => #{self.id}, :info => #{self.info.inspect}, "\ ":connection => #{self.connection} }" end # #json returns extra information about an Image, #history returns its # history. [:json, :history].each do |method| define_method(method) do |opts = {}| Docker::Util.parse_json(connection.get(path_for(method), opts)) end end # Update the @info hash, which is the only mutable state in this object. def refresh! img = Docker::Image.all(:all => true).find { |image| image.id.start_with?(self.id) || self.id.start_with?(image.id) } info.merge!(self.json) img && info.merge!(img.info) self end class << self # Create a new Image. def create(opts = {}, creds = nil, conn = Docker.connection) credentials = creds.nil? ? Docker.creds : creds.to_json headers = !credentials.nil? && Docker::Util.build_auth_header(credentials) headers ||= {} body = conn.post('/images/create', opts, :headers => headers) id = Docker::Util.fix_json(body).last['id'] new(conn, 'id' => id, :headers => headers) end # Return a specific image. def get(id, opts = {}, conn = Docker.connection) image_json = conn.get("/images/#{URI.encode(id)}/json", opts) hash = Docker::Util.parse_json(image_json) || {} new(conn, hash) end # Return every Image. def all(opts = {}, conn = Docker.connection) hashes = Docker::Util.parse_json(conn.get('/images/json', opts)) || [] hashes.map { |hash| new(conn, hash) } end # Given a query like `{ :term => 'sshd' }`, queries the Docker Registry for # a corresponding Image. def search(query = {}, connection = Docker.connection) body = connection.get('/images/search', query) hashes = Docker::Util.parse_json(body) || [] hashes.map { |hash| new(connection, 'id' => hash['name']) } end # Import an Image from the output of Docker::Container#export. The first # argument may either be a File or URI. def import(imp, opts = {}, conn = Docker.connection) open(imp) do |io| import_stream(opts, conn) do io.read(Excon.defaults[:chunk_size]).to_s end end rescue StandardError raise Docker::Error::IOError, "Could not import '#{imp}'" end def import_stream(options = {}, connection = Docker.connection, &block) body = connection.post( '/images/create', options.merge('fromSrc' => '-'), :headers => { 'Content-Type' => 'application/tar', 'Transfer-Encoding' => 'chunked' }, &block ) new(connection, 'id'=> Docker::Util.parse_json(body)['status']) end # Given a Dockerfile as a string, builds an Image. def build(commands, opts = {}, connection = Docker.connection, &block) body = "" connection.post( '/build', opts, :body => Docker::Util.create_tar('Dockerfile' => commands), :response_block => response_block_for_build(body, &block) ) new(connection, 'id' => Docker::Util.extract_id(body)) rescue Docker::Error::ServerError raise Docker::Error::UnexpectedResponseError end # Given a directory that contains a Dockerfile, builds an Image. # # If a block is passed, chunks of output produced by Docker will be passed # to that block. def build_from_dir(dir, opts = {}, connection = Docker.connection, creds = nil, &block) tar = Docker::Util.create_dir_tar(dir) headers = build_headers(creds) # The response_block passed to Excon will build up this body variable. body = "" connection.post( '/build', opts, :headers => headers, :response_block => response_block_for_build(body, &block) ) { tar.read(Excon.defaults[:chunk_size]).to_s } new(connection, 'id' => Docker::Util.extract_id(body), :headers => headers) ensure unless tar.nil? tar.close FileUtils.rm(tar.path, force: true) end end end private # A method to build the config header and merge it into the # headers sent by build_from_dir. def self.build_headers(creds) credentials = creds || Docker.creds || {} config_header = Docker::Util.build_config_header(credentials) headers = { 'Content-Type' => 'application/tar', 'Transfer-Encoding' => 'chunked' } headers = headers.merge(config_header) if config_header headers end # Convenience method to return the path for a particular resource. def path_for(resource) "/images/#{self.id}/#{resource}" end # Convience method to get the Dockerfile for a file hash and a path to # output to. def dockerfile_for(file_hash, output_path) dockerfile = "from #{self.id}\n" file_hash.keys.each do |basename| dockerfile << "add #{basename} #{output_path}\n" end dockerfile end # Generates the block to be passed as a reponse block to Excon. The returned # lambda will append Docker output to the first argument, and yield output to # the passed block, if a block is given. def self.response_block_for_build(body) lambda do |chunk, remaining, total| body << chunk yield chunk if block_given? end end end