File: repository.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 (264 lines) | stat: -rwxr-xr-x 9,479 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
260
261
262
263
264
# -*- coding: utf-8 -*-

"""
A repository is a collection of ``ImageSource`` s which are published in a git repo.
"""

#external imports
import os
import shutil
import json
from collections import OrderedDict
#internal imports
from subuserlib.classes.userOwnedObject import UserOwnedObject
from subuserlib.classes.imageSource import ImageSource
from subuserlib.classes.describable import Describable
from subuserlib.classes.gitRepository import GitRepository
from subuserlib.classes.fileStructure import BasicFileStructure
import subuserlib.version

class Repository(OrderedDict,UserOwnedObject,Describable):
  def __init__(self,user,name,gitOriginURI=None,gitCommitHash=None,temporary=False,sourceDir=None):
    """
    Repositories can either be managed by git, or simply be normal directories on the user's computer. If ``sourceDir`` is not set to None, then ``gitOriginURI`` is ignored and the repository is assumed to be a simple directory.
    """
    self.name = name
    self.gitOriginURI = gitOriginURI
    self.gitCommitHash = gitCommitHash
    self.temporary = temporary
    self.sourceDir = sourceDir
    self.__fileStructure = None
    UserOwnedObject.__init__(self,user)
    super().__init__()
    self.gitRepository = GitRepository(user,self.repoPath)
    if not self.isPresent():
      self.updateSources(initialUpdate=True)
    if self.isPresent():
      self.repoConfig = self.loadRepoConfig()
      self.loadImageSources()

  @property
  def uri(self):
    if self.isLocal:
      return self.sourceDir
    else:
      return self.gitOriginURI

  @property
  def fileStructure(self):
    if self.__fileStructure is None:
      if self.isLocal:
        self.__fileStructure = BasicFileStructure(self.sourceDir)
      else:
        if self.gitCommitHash is None:
          self.updateGitCommitHash()
        self.__fileStructure = self.gitRepository.getFileStructureAtCommit(self.gitCommitHash)
    return self.__fileStructure

  @property
  def displayName(self):
    """
    How should we refer to this repository when communicating with the user?
    """
    if self.temporary:
      if self.isLocal:
        return self.sourceDir
      else:
        return self.gitOriginURI
    else:
      return self.name

  def describe(self):
    print("Repository: "+self.displayName)
    print("------------")
    if self.isLocal:
      print("Is a local(non-git) repository.")
      if not self.temporary:
        print("Located at: " + self.repoPath)
    if self.temporary:
      print("Is a temporary repository.")
    else:
      if not self.isLocal:
        print("Cloned from: "+self.gitOriginURI)
        print("Currently at commit: "+self.gitCommitHash)

  def getSortedList(self):
    """
    Return a list of image sources sorted by name.
    """
    return list(sorted(self.values(),key=lambda imageSource:imageSource.name))

  @property
  def repoPath(self):
    """ Get the path of the repo's sources on disk. """
    if self.isLocal:
      return self.sourceDir
    else:
      return os.path.join(self.user.config["repositories-dir"],self.name)

  def loadRepoConfig(self):
    """
    Either returns the config as a dictionary or None if no configuration exists or can be parsed.
    """
    def verifyPaths(dictionary,paths):
      """
      Looks through a dictionary at all entries that can be considered to be paths, and ensures that they do not contain any relative upwards references.
      Throws a ValueError if they do.
      """
      for path in paths:
        if path in dictionary and path.startswith("../") or "/../" in path:
          raise ValueError("Paths in .subuser.json may not be relative to a higher directory.")
    if self.fileStructure.exists("./.subuser.json"):
      configFileContents = self.fileStructure.read("./.subuser.json")
    else:
      return None
    repoConfig = json.loads(configFileContents)
    # Validate untrusted input
    verifyPaths(repoConfig,["image-sources-dir"])
    if "explicit-image-sources" in repoConfig:
      for explicitImageSource in repoConfig["explicit-image-sources"]:
        verifyPaths(explicitImageSource,["image-file","permissions-file","build-context"])
    return repoConfig

  @property
  def imageSourcesDir(self):
    """
    Get the path of the repo's subuser root on disk on the host.
    """
    return os.path.join(self.repoPath,self.relativeImageSourcesDir)

  @property
  def relativeImageSourcesDir(self):
    """
    Get the path of the repo's subuser root on disk on the host.
    """
    repoConfig = self.repoConfig
    if repoConfig and "image-sources-dir" in repoConfig:
      return repoConfig["image-sources-dir"]
    else:
      return "./"

  def isInUse(self):
    """
    Are there any installed images or subusers from this repository?
    """
    for _,installedImage in self.user.installedImages.items():
      if self.name == installedImage.sourceRepoId:
          return True
      for _,subuser in self.user.registry.subusers.items():
        try:
          if self.name == subuser.imageSource.repo.name:
            return True
        except subuserlib.classes.subuser.NoImageSourceException:
          pass
    return False

  @property
  def isLocal(self):
    if self.sourceDir:
      return True
    return False

  def removeGitRepo(self):
    """
    Remove the downloaded git repo associated with this repository from disk.
    """
    if not self.isLocal:
      shutil.rmtree(self.repoPath)

  def isPresent(self):
    """
    Returns True if the repository's files are present on the system. (Cloned or local)
    """
    return os.path.exists(self.repoPath)

  def updateSources(self,initialUpdate=False):
    """
    Pull(or clone) the repo's ImageSources from git origin.
    """
    if self.isLocal:
      return
    if not self.isPresent():
      new = True
      self.user.registry.log("Cloning repository "+self.name+" from "+self.gitOriginURI)
      if self.gitRepository.clone(self.gitOriginURI) != 0:
        self.user.registry.log("Clone failed.")
        return
    else:
      new = False
    try:
      self.gitRepository.run(["fetch","--all"])
    except Exception: # For some reason, git outputs normal messages to stderr.
      pass
    if self.updateGitCommitHash():
      if not new:
        self.user.registry.logChange("Updated repository "+self.displayName)
      if not initialUpdate:
        self.loadImageSources()

  def serializeToDict(self):
    """
    Return a dictionary which describes the image sources available in this repository.
    """
    imageSourcesDict = {}
    for name,imageSource in self.items():
      imageSourcesDict[name] = imageSource.serializeToDict()
    return imageSourcesDict

  def loadImageSources(self):
    """
    Load ImageSources from disk into memory.
    """
    imageNames = self.fileStructure.lsFolders(self.relativeImageSourcesDir)
    for imageName in imageNames:
      imageSource = ImageSource(self.user,self,imageName)
      if self.fileStructure.exists(imageSource.getRelativePermissionsFilePath()):
        self[imageName] = imageSource
    if self.repoConfig is not None and "explicit-image-sources" in self.repoConfig:
      for imageName,config in self.repoConfig["explicit-image-sources"].items():
        assert config is not None
        self[imageName] = ImageSource(self.user,self,imageName,explicitConfig = config)

  def updateGitCommitHash(self):
    """
    Update the internally stored git commit hash to the current git HEAD of the repository.
    Returns True if the repository has been updated.
    Otherwise false.
    """
    if self.isLocal:
      return True
    # Default
    master = "refs/remotes/origin/master"
    newCommitHash = self.gitRepository.getHashOfRef(master)
    # First we check for version constraints on the repository.
    if self.gitRepository.getFileStructureAtCommit(master).exists("./.subuser.json"):
      configFileContents = self.gitRepository.getFileStructureAtCommit(master).read("./.subuser.json")
      configAtMaster = json.loads(configFileContents)
      if "subuser-version-constraints" in configAtMaster:
        versionConstraints = configAtMaster["subuser-version-constraints"]
        subuserVersion = subuserlib.version.getSubuserVersion(self.user)
        for constraint in versionConstraints:
          if not len(constraint) == 3:
            raise SyntaxError("Error in .subuser.json file. Invalid subuser-version-constraints."+ str(versionConstraints))
          op,version,commit = constraint
          from operator import lt,le,eq,ge,gt
          operators = {"<":lt,"<=":le,"==":eq,">=":ge,">":gt}
          try:
            matched = operators[op](subuserVersion,version)
          except KeyError:
            raise SyntaxError("Error in .subuser.json file. Invalid subuser-version-constraints.  \""+op+"\" is not a valid operator.\n\n"+ str(versionConstraints))
          if matched:
            try:
              newCommitHash = self.gitRepository.getHashOfRef("refs/remotes/origin/"+commit)
            except OSError as e:
              if len(commit) == 40:
                newCommitHash = commit
              else:
                raise e
            break
        else:
          raise SyntaxError("Error reading .subuser.json file, no version constraints matched the current subuser version ("+subuserVersion+").\n\n"+str(versionConstraints))
    updated = not (newCommitHash == self.gitCommitHash)
    self.gitCommitHash = newCommitHash
    self.__fileStructure = None
    return updated