File: dockerDaemon.py

package info (click to toggle)
subuser 0.6.2-3
  • links: PTS
  • area: main
  • in suites: bookworm, bullseye, buster, forky, sid, trixie
  • size: 4,208 kB
  • sloc: python: 5,201; sh: 380; makefile: 73
file content (259 lines) | stat: -rwxr-xr-x 10,766 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# -*- coding: utf-8 -*-

"""
The DockerDaemon object allows us to communicate with the Docker daemon via the Docker HTTP REST API.
"""

#external imports
import urllib
import tarfile
import os
import tempfile
import fnmatch
import re
import json
import sys
try:
  import httplib
except ImportError:
  import http.client
  httplib = http.client
try:
  import StringIO
except ImportError:
  import io
#internal imports
from subuserlib.classes.userOwnedObject import UserOwnedObject
from subuserlib.classes.uhttpConnection import UHTTPConnection
import subuserlib.docker
import subuserlib.test
from subuserlib.classes.docker.container import Container
import subuserlib.classes.exceptions as exceptions

def archiveBuildContext(archive,relativeBuildContextPath,repositoryFileStructure,excludePatterns,dockerfile=None):
  """
  Archive files from directoryWithDockerfile into the FileObject archive excluding files who's paths(relative to directoryWithDockerfile) are in excludePatterns.
  If dockerfile is set to a string, include that string as the file Dockerfile in the archive.
  """
  def getFileObject(contents):
    """
    Returns a FileObject from the given string. Works with both versions of python.
    """
    return io.BytesIO(contents)

  def addFileFromContents(path,contents,mode=420):
    fileObject = getFileObject(contents)
    tarinfo = tarfile.TarInfo(name=path)
    tarinfo.mode=mode
    fileObject.seek(0, os.SEEK_END)
    tarinfo.size = fileObject.tell()
    fileObject.seek(0)
    contexttarfile.addfile(tarinfo,fileObject)
  # Inspired by and partialy taken from https://github.com/docker/docker-py
  contexttarfile = tarfile.open(mode="w",fileobj=archive)
  if relativeBuildContextPath and repositoryFileStructure:
    def addFolder(folder):
      for filename in repositoryFileStructure.lsFiles(folder):
        filePathRelativeToRepository = os.path.join(folder,filename)
        filePathRelativeToBuildContext = os.path.relpath(filePathRelativeToRepository,relativeBuildContextPath)
        exclude = False
        for excludePattern in excludePatterns:
          if fnmatch.fnmatch(filePathRelativeToBuildContext,excludePattern):
            exclude = True
            break
        if not exclude:
          addFileFromContents(path=filePathRelativeToBuildContext,contents=repositoryFileStructure.readBinary(filePathRelativeToRepository),mode=repositoryFileStructure.getMode(filePathRelativeToRepository))
      for subFolder in repositoryFileStructure.lsFolders(folder):
        addFolder(os.path.join(folder,subFolder))
    addFolder(relativeBuildContextPath)
  # Add the provided Dockerfile if necessary
  if not dockerfile == None:
    addFileFromContents(path="./Dockerfile",contents=dockerfile.encode("utf-8"))
  contexttarfile.close()
  archive.seek(0)

def readAndPrintStreamingBuildStatus(user,response):
  jsonSegmentBytes = b''
  output = b''
  byte = response.read(1)
  while byte:
    jsonSegmentBytes += byte
    output += byte
    byte = response.read(1)
    try:
      lineDict = json.loads(jsonSegmentBytes.decode("utf-8"))
      if lineDict == {}:
        pass
      elif "stream" in lineDict:
        user.registry.log(lineDict["stream"])
      elif "status" in lineDict:
        user.registry.log(lineDict["status"])
      elif "errorDetail" in lineDict:
        raise exceptions.ImageBuildException("Build error:"+lineDict["errorDetail"]["message"]+"\n"+response.read().decode())
      else:
        raise exceptions.ImageBuildException("Build error:"+jsonSegmentBytes.decode("utf-8")+"\n"+response.read().decode("utf-8"))
      jsonSegmentBytes = b''
    except ValueError:
      pass
  output = output.decode("utf-8")
  if not output.strip().startswith("{"):
    user.registry.log(output)
  return output

class DockerDaemon(UserOwnedObject):
  def __init__(self,user):
    self.__connection = None
    self.__imagePropertiesCache = {}
    UserOwnedObject.__init__(self,user)

  def getConnection(self):
    """
     Get an `HTTPConnection <https://docs.python.org/2/library/httplib.html#httplib.HTTPConnection>`_ to the Docker daemon.

     Note: You can find more info in the `Docker API docs <https://docs.docker.com/reference/api/docker_remote_api_v1.13/>`_
    """
    if not self.__connection:
      subuserlib.docker.getAndVerifyExecutable()
      try:
        self.__connection = UHTTPConnection("/var/run/docker.sock")
      except PermissionError as e:
        sys.exit("Permission error (%s) connecting to the docker socket. This usually happens when you've added yourself as a member of the docker group but haven't logged out/in again before starting subuser."% str(e))
    return self.__connection

  def getContainers(self,onlyRunning=False):
    queryParameters =  {'all': not onlyRunning}
    queryParametersString = urllib.parse.urlencode(queryParameters)
    self.getConnection().request("GET","/v1.13/containers/json?"+queryParametersString)
    response = self.getConnection().getresponse()
    if response.status == 200:
      return json.loads(response.read().decode("utf-8"))
    else:
      return []

  def getContainer(self,containerId):
    return Container(self.user,containerId)

  def getImageProperties(self,imageTagOrId):
    """
     Returns a dictionary of image properties, or None if the image does not exist.
    """
    try:
      return self.__imagePropertiesCache[imageTagOrId]
    except KeyError:
      pass
    self.getConnection().request("GET","/v1.13/images/"+imageTagOrId+"/json")
    response = self.getConnection().getresponse()
    if not response.status == 200:
      response.read() # Read the response and discard it to prevent the server from getting locked up: https://stackoverflow.com/questions/3231543/python-httplib-responsenotready
      return None
    else:
      properties = json.loads(response.read().decode("utf-8"))
      self.__imagePropertiesCache[imageTagOrId] = properties
      return properties

  def removeImage(self,imageId):
    self.getConnection().request("DELETE","/v1.13/images/"+imageId)
    response = self.getConnection().getresponse()
    if response.status == 404:
      raise ImageDoesNotExistsException("The image "+imageId+" could not be deleted.\n"+response.read().decode("utf-8"))
    elif response.status == 409:
      raise ContainerDependsOnImageException("The image "+imageId+" could not be deleted.\n"+response.read().decode("utf-8"))
    elif response.status == 500:
      raise ServerErrorException("The image "+imageId+" could not be deleted.\n"+response.read().decode("utf-8"))
    else:
      response.read()

  def build(self,relativeBuildContextPath=None,repositoryFileStructure=None,useCache=True,rm=True,forceRm=True,quiet=False,tag=None,dockerfile=None,quietClient=False):
    """
    Build a Docker image.  If a the dockerfile argument is set to a string, use that string as the Dockerfile.  Returns the newly created images Id or raises an exception if the build fails.

    Most of the options are passed directly on to Docker.

    The quietClient option makes it so that this function does not print any of Docker's status messages when building.
    """
    # Inspired by and partialy taken from https://github.com/docker/docker-py
    queryParameters =  {
      'q': quiet,
      'nocache': not useCache,
      'rm': rm,
      'forcerm': forceRm
      }
    if tag:
      queryParameters["t"] = tag
    queryParametersString = urllib.parse.urlencode(queryParameters)
    excludePatterns = []
    if relativeBuildContextPath and repositoryFileStructure:
      dockerignore = "./.dockerignore"
      if repositoryFileStructure.exists(dockerignore):
        exclude = list(filter(bool, repositoryFileStructure.read(dockerignore).split('\n')))
    with tempfile.NamedTemporaryFile() as tmpArchive:
      archiveBuildContext(tmpArchive,relativeBuildContextPath=relativeBuildContextPath,repositoryFileStructure=repositoryFileStructure,excludePatterns=excludePatterns,dockerfile=dockerfile)
      self.getConnection().request("POST","/v1.18/build?"+queryParametersString,body=tmpArchive)
    try:
      response = self.getConnection().getresponse()
    except httplib.ResponseNotReady as rnr:
      raise exceptions.ImageBuildException(rnr)
    if response.status != 200:
      if quietClient:
        response.read()
      else:
        readAndPrintStreamingBuildStatus(self.user, response)
      raise exceptions.ImageBuildException("Building image failed.\n"
                     +"status: "+str(response.status)+"\n"
                     +"Reason: "+response.reason+"\n")
    if quietClient:
      output = response.read().decode("utf-8")
    else:
      output = readAndPrintStreamingBuildStatus(self.user,response)
    # Now we move to regex code stolen from the official python Docker bindings. This is REALLY UGLY!
    outputLines = output.split("\n")
    search = r'Successfully built ([0-9a-f]+)' #This is REALLY ugly!
    match = None
    for line in reversed(outputLines):
      match = re.search(search, line) #This is REALLY ugly!
      if match:
        break
    if not match:
      raise exceptions.ImageBuildException("Unexpected server response when building image.")
    shortId = match.group(1) #This is REALLY ugly!
    return self.getImageProperties(shortId)["Id"]

  def getInfo(self):
    """
    Returns a dictionary of version info about the running Docker daemon.
    """
    self.getConnection().request("GET","/v1.13/info")
    response = self.getConnection().getresponse()
    if not response.status == 200:
      response.read() # Read the response and discard it to prevent the server from getting locked up: https://stackoverflow.com/questions/3231543/python-httplib-responsenotready
      return None
    else:
      return json.loads(response.read().decode("utf-8"))

  def execute(self,args,cwd=None,background=False,backgroundSuppressOutput=True,backgroundCollectStdout=False,backgroundCollectStderr=False):
    """
    Execute the docker client.
    If the background argument is True, return emediately with the docker client's subprocess.
    Otherwise, wait for the process to finish and return the docker client's exit code.
    """
    if background:
      return subuserlib.docker.runBackground(args,cwd=cwd,suppressOutput=backgroundSuppressOutput,collectStdout=backgroundCollectStdout,collectStderr=backgroundCollectStderr)
    else:
      return subuserlib.docker.run(args,cwd=cwd)

class ImageBuildException(Exception):
  pass

class ImageDoesNotExistsException(Exception):
  pass

class ContainerDependsOnImageException(Exception):
  pass

class ServerErrorException(Exception):
  pass

if subuserlib.test.testing:
  from subuserlib.classes.docker.mockDockerDaemon import MockDockerDaemon
  RealDockerDaemon = DockerDaemon
  DockerDaemon = MockDockerDaemon