Skip to content
This repository was archived by the owner on Jan 19, 2018. It is now read-only.

Commit 2b882e1

Browse files
committed
Add "index" command to Atomic App
This adds an index command to list multiple Nulecule's from an external library that contains an index.yaml file in it's root directory.
1 parent 98e0ba4 commit 2b882e1

5 files changed

Lines changed: 297 additions & 1 deletion

File tree

atomicapp/cli/main.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from atomicapp.nulecule.exceptions import NuleculeException, DockerException
3838
from atomicapp.plugin import ProviderFailedException
3939
from atomicapp.utils import Utils
40+
from atomicapp.index import Index
4041

4142
logger = logging.getLogger(LOGGER_DEFAULT)
4243

@@ -112,6 +113,19 @@ def cli_init(args):
112113
sys.exit(1)
113114

114115

116+
def cli_index(args):
117+
argdict = args.__dict__
118+
i = Index()
119+
if argdict["index_action"] == "list":
120+
if argdict["update"]:
121+
i.update()
122+
else:
123+
i.list()
124+
elif argdict["index_action"] == "generate":
125+
i.generate(argdict["location"])
126+
sys.exit(0)
127+
128+
115129
# Create a custom action parser. Need this because for some args we don't
116130
# want to store a value if the user didn't provide one. "store_true" does
117131
# not allow this; it will always create an attribute and store a value.
@@ -381,6 +395,28 @@ def create_parser(self):
381395
help='The name of a container image containing an Atomic App.')
382396
gena_subparser.set_defaults(func=cli_genanswers)
383397

398+
# === "index" SUBPARSER ===
399+
index_subparser = toplevel_subparsers.add_parser(
400+
"index", parents=[globals_parser])
401+
index_action = index_subparser.add_subparsers(dest="index_action")
402+
403+
index_list = index_action.add_parser("list")
404+
index_list.add_argument(
405+
"--update",
406+
dest="update",
407+
default=False,
408+
action="store_true",
409+
help=("Update the index list"))
410+
index_list.set_defaults(func=cli_index)
411+
412+
index_generate = index_action.add_parser("generate")
413+
index_generate.add_argument(
414+
"location",
415+
help=(
416+
"Path or Git repository link containing Nulecule applications "
417+
"which will be part of the genrated index"))
418+
index_generate.set_defaults(func=cli_index)
419+
384420
# === "init" SUBPARSER ===
385421
init_subparser = toplevel_subparsers.add_parser(
386422
"init", parents=[globals_parser])
@@ -465,7 +501,7 @@ def run(self):
465501
# a directory if they want to for "run". For that reason we won't
466502
# default the RUN label for Atomic App to provide an app_spec argument.
467503
# In this case pick up app_spec from $IMAGE env var (set by RUN label).
468-
if args.action != 'init' and args.app_spec is None:
504+
if args.action != 'init' and args.action != 'index' and args.app_spec is None:
469505
if os.environ.get('IMAGE') is not None:
470506
logger.debug("Setting app_spec based on $IMAGE env var")
471507
args.app_spec = os.environ['IMAGE']

atomicapp/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,10 @@
8282

8383
# If running in an openshift POD via `oc new-app`, the ca file is here
8484
OPENSHIFT_POD_CA_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
85+
86+
# Index
87+
INDEX_IMAGE = "projectatomic/nulecule-library"
88+
INDEX_DEFAULT_IMAGE_LOCATION = "localhost"
89+
INDEX_NAME = "index.yaml"
90+
INDEX_LOCATION = ".atomicapp/" + INDEX_NAME
91+
INDEX_GEN_DEFAULT_OUTPUT_LOC = "./" + INDEX_NAME

atomicapp/index.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Copyright 2014-2016 Red Hat, Inc.
3+
4+
This file is part of Atomic App.
5+
6+
Atomic App is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Lesser General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
Atomic App is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Lesser General Public License for more details.
15+
16+
You should have received a copy of the GNU Lesser General Public License
17+
along with Atomic App. If not, see <http://www.gnu.org/licenses/>.
18+
"""
19+
20+
from __future__ import print_function
21+
import os
22+
23+
import logging
24+
import errno
25+
from constants import (INDEX_IMAGE,
26+
INDEX_LOCATION,
27+
INDEX_DEFAULT_IMAGE_LOCATION,
28+
INDEX_GEN_DEFAULT_OUTPUT_LOC,
29+
INDEX_NAME)
30+
from nulecule.container import DockerHandler
31+
from nulecule.base import Nulecule
32+
33+
from copy import deepcopy
34+
35+
import anymarkup
36+
from atomicapp.utils import Utils
37+
38+
logger = logging.getLogger(__name__)
39+
40+
41+
class IndexException(Exception):
42+
pass
43+
44+
45+
class Index(object):
46+
47+
"""
48+
This class represents the 'index' command for Atomic App. This lists
49+
all available packaged applications to use.
50+
"""
51+
52+
index_template = {"location": ".", "nulecules": []}
53+
54+
def __init__(self):
55+
56+
self.index = deepcopy(self.index_template)
57+
self.index_location = os.path.join(Utils.getUserHome(), INDEX_LOCATION)
58+
self._load_index_file(self.index_location)
59+
60+
def list(self):
61+
"""
62+
This command lists all available Nulecule packaged applications in a
63+
properly formatted way.
64+
"""
65+
66+
# In order to "format" it correctly, find the largest length of 'name', 'id', and 'appversion'
67+
# Set a minimum length of '7' due to the length of each column name
68+
id_length = 7
69+
app_length = 7
70+
location_length = 7
71+
72+
# Loop through each 'nulecule' and retrieve the largest string length
73+
for entry in self.index["nulecules"]:
74+
id = entry.get('id') or ""
75+
version = entry['metadata'].get('appversion') or ""
76+
location = entry['metadata'].get('location') or INDEX_DEFAULT_IMAGE_LOCATION
77+
78+
if len(id) > id_length:
79+
id_length = len(id)
80+
if len(version) > app_length:
81+
app_length = len(version)
82+
if len(location) > location_length:
83+
location_length = len(location)
84+
85+
# Print out the "index bar" with the lengths
86+
index_format = ("{0:%s} {1:%s} {2:10} {3:%s}" % (id_length, app_length, location_length))
87+
print(index_format.format("ID", "VER", "PROVIDERS", "LOCATION"))
88+
89+
# Loop through each entry of the index and spit out the formatted line
90+
for entry in self.index["nulecules"]:
91+
# Get the list of providers (first letter)
92+
providers = ""
93+
for provider in entry["providers"]:
94+
providers = "%s,%s" % (providers, provider[0].capitalize())
95+
96+
# Remove the first element, add brackets
97+
providers = "{%s}" % providers[1:]
98+
99+
# Retrieve the entry information
100+
id = entry.get('id') or ""
101+
version = entry['metadata'].get('appversion') or ""
102+
location = entry['metadata'].get('location') or INDEX_DEFAULT_IMAGE_LOCATION
103+
104+
# Print out the row
105+
print(index_format.format(
106+
id,
107+
version,
108+
providers,
109+
location))
110+
111+
def update(self, index_image=INDEX_IMAGE):
112+
"""
113+
Fetch the latest index image and update the file based upon
114+
the INDEX_IMAGE attribute. By default, this should pull the
115+
'official' Nulecule index.
116+
"""
117+
118+
logger.info("Updating the index list")
119+
logger.info("Pulling latest index image...")
120+
self._fetch_index_container()
121+
logger.info("Index updated")
122+
123+
# TODO: Error out if the locaiton does not have a Nulecule file / dir
124+
def generate(self, location, output_location=INDEX_GEN_DEFAULT_OUTPUT_LOC):
125+
"""
126+
Generate an index.yaml with a provided directory location
127+
"""
128+
logger.info("Generating index.yaml from %s" % location)
129+
self.index = deepcopy(self.index_template)
130+
131+
if not os.path.isdir(location):
132+
raise Exception("Location must be a directory")
133+
134+
for f in os.listdir(location):
135+
nulecule_dir = os.path.join(location, f)
136+
if f.startswith("."):
137+
continue
138+
if os.path.isdir(nulecule_dir):
139+
index_info = self._nulecule_get_info(nulecule_dir)
140+
index_info["path"] = f
141+
self.index["nulecules"].append(index_info)
142+
143+
if len(index_info) > 0:
144+
anymarkup.serialize_file(self.index, output_location, format="yaml")
145+
logger.info("index.yaml generated")
146+
147+
def _fetch_index_container(self, index_image=INDEX_IMAGE):
148+
"""
149+
Fetch the index container
150+
"""
151+
# Create the ".atomicapp" dir if it does not exist
152+
if not os.path.exists(os.path.dirname(self.index_location)):
153+
try:
154+
os.makedirs(os.path.dirname(self.index_location))
155+
except OSError as exc: # Guard against race condition
156+
if exc.errno != errno.EEXIST:
157+
raise
158+
159+
dh = DockerHandler()
160+
dh.pull(index_image)
161+
dh.extract(index_image, "/" + INDEX_NAME, self.index_location, file=True)
162+
163+
def _load_index_file(self, index_file=INDEX_LOCATION):
164+
"""
165+
Load the index file. If it does not exist, fetch it.
166+
"""
167+
# If the file/path does not exist, retrieve the index yaml
168+
if not os.path.exists(index_file):
169+
logger.warning("Couldn't load index file: %s", index_file)
170+
logger.info("Retrieving index...")
171+
self._fetch_index_container()
172+
self.index = anymarkup.parse_file(index_file)
173+
174+
def _nulecule_get_info(self, nulecule_dir):
175+
"""
176+
Get the required information in order to generate an index.yaml
177+
"""
178+
index_info = {}
179+
nulecule = Nulecule.load_from_path(
180+
nulecule_dir, nodeps=True)
181+
index_info["id"] = nulecule.id
182+
index_info["metadata"] = nulecule.metadata
183+
index_info["specversion"] = nulecule.specversion
184+
185+
if len(nulecule.components) == 0:
186+
raise IndexException("Unable to load any Nulecule components from path")
187+
188+
providers_set = set()
189+
for component in nulecule.components:
190+
if component.artifacts:
191+
if len(providers_set) == 0:
192+
providers_set = set(component.artifacts.keys())
193+
else:
194+
providers_set = providers_set.intersection(set(component.artifacts.keys()))
195+
196+
index_info["providers"] = list(providers_set)
197+
return index_info

atomicapp/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from __future__ import print_function
2121
import distutils.dir_util
22+
import shutil
2223
import os
2324
import sys
2425
import tempfile
@@ -371,6 +372,11 @@ def copy_dir(src, dest, update=False, dryrun=False):
371372
if not dryrun:
372373
distutils.dir_util.copy_tree(src, dest, update)
373374

375+
@staticmethod
376+
def copy_file(src, dest, dryrun=False):
377+
if not dryrun:
378+
shutil.copy(src, dest)
379+
374380
@staticmethod
375381
def rm_dir(directory):
376382
logger.debug('Recursively removing directory: %s' % directory)

tests/units/index/test_index.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Copyright 2014-2016 Red Hat, Inc.
3+
4+
This file is part of Atomic App.
5+
6+
Atomic App is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Lesser General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
Atomic App is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Lesser General Public License for more details.
15+
16+
You should have received a copy of the GNU Lesser General Public License
17+
along with Atomic App. If not, see <http://www.gnu.org/licenses/>.
18+
"""
19+
20+
import unittest
21+
import mock
22+
import os
23+
import tempfile
24+
25+
from atomicapp.index import Index
26+
27+
28+
def mock_index_load_call(self, test):
29+
self.index = {'location': '.', 'nulecules': [
30+
{'providers': ['docker'], 'id': 'test', 'metadata':{'appversion': '0.0.1', 'location': 'foo'}}]}
31+
32+
33+
class TestIndex(unittest.TestCase):
34+
35+
"""
36+
Tests the index
37+
"""
38+
39+
# Tests listing the index with a patched self.index
40+
@mock.patch("atomicapp.index.Index._load_index_file", mock_index_load_call)
41+
def test_list(self):
42+
a = Index()
43+
a.list()
44+
45+
# Test generation with current test_examples in cli
46+
@mock.patch("atomicapp.index.Index._load_index_file", mock_index_load_call)
47+
def test_generate(self):
48+
self.tmpdir = tempfile.mkdtemp(prefix="atomicapp-generation-test", dir="/tmp")
49+
a = Index()
50+
a.generate("tests/units/cli/test_examples", os.path.join(self.tmpdir, "index.yaml"))

0 commit comments

Comments
 (0)