Skip to content

Commit 59a3d1c

Browse files
authored
Merge pull request #146 from splunk/enable_acs_deploy
Enable acs deploy + appinspect warnings
2 parents 7f5319e + dd77dc6 commit 59a3d1c

File tree

12 files changed

+215
-97
lines changed

12 files changed

+215
-97
lines changed

contentctl/actions/build.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
5151
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.lookups, SecurityContentType.lookups))
5252
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.macros, SecurityContentType.macros))
5353
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.dashboards, SecurityContentType.dashboards))
54-
updated_conf_files.update(conf_output.writeAppConf())
54+
updated_conf_files.update(conf_output.writeMiscellaneousAppFiles())
55+
56+
5557

5658
#Ensure that the conf file we just generated/update is syntactically valid
5759
for conf_file in updated_conf_files:

contentctl/actions/deploy_acs.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,55 @@
1-
from dataclasses import dataclass
2-
from contentctl.input.director import DirectorInputDto
3-
from contentctl.output.conf_output import ConfOutput
4-
5-
6-
from typing import Union
7-
8-
@dataclass(frozen=True)
9-
class ACSDeployInputDto:
10-
director_input_dto: DirectorInputDto
11-
splunk_api_username: str
12-
splunk_api_password: str
13-
splunk_cloud_jwt_token: str
14-
splunk_cloud_stack: str
15-
stack_type: str
1+
from contentctl.objects.config import deploy_acs, StackType
2+
from requests import post
3+
import pprint
164

175

186
class Deploy:
19-
def execute(self, input_dto: ACSDeployInputDto) -> None:
20-
21-
conf_output = ConfOutput(input_dto.director_input_dto.input_path, input_dto.director_input_dto.config)
7+
def execute(self, config: deploy_acs, appinspect_token:str) -> None:
228

23-
appinspect_token = conf_output.inspectAppAPI(input_dto.splunk_api_username, input_dto.splunk_api_password, input_dto.stack_type)
9+
#The following common headers are used by both Clasic and Victoria
10+
headers = {
11+
'Authorization': f'Bearer {config.splunk_cloud_jwt_token}',
12+
'ACS-Legal-Ack': 'Y'
13+
}
14+
try:
15+
16+
with open(config.getPackageFilePath(include_version=False),'rb') as app_data:
17+
#request_data = app_data.read()
18+
if config.stack_type == StackType.classic:
19+
# Classic instead uses a form to store token and package
20+
# https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps#Manage_private_apps_using_the_ACS_API_on_Classic_Experience
21+
address = f"https://admin.splunk.com/{config.splunk_cloud_stack}/adminconfig/v2/apps"
22+
23+
form_data = {
24+
'token': (None, appinspect_token),
25+
'package': app_data
26+
}
27+
res = post(address, headers=headers, files = form_data)
28+
elif config.stack_type == StackType.victoria:
29+
# Victoria uses the X-Splunk-Authorization Header
30+
# It also uses --data-binary for the app content
31+
# https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps#Manage_private_apps_using_the_ACS_API_on_Victoria_Experience
32+
headers.update({'X-Splunk-Authorization': appinspect_token})
33+
address = f"https://admin.splunk.com/{config.splunk_cloud_stack}/adminconfig/v2/apps/victoria"
34+
res = post(address, headers=headers, data=app_data.read())
35+
else:
36+
raise Exception(f"Unsupported stack type: '{config.stack_type}'")
37+
except Exception as e:
38+
raise Exception(f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{str(e)}")
2439

25-
26-
if input_dto.splunk_cloud_jwt_token is None or input_dto.splunk_cloud_stack is None:
27-
if input_dto.splunk_cloud_jwt_token is None:
28-
raise Exception("Cannot deploy app via ACS, --splunk_cloud_jwt_token was not defined on command line.")
29-
else:
30-
raise Exception("Cannot deploy app via ACS, --splunk_cloud_stack was not defined on command line.")
31-
32-
conf_output.deploy_via_acs(input_dto.splunk_cloud_jwt_token,
33-
input_dto.splunk_cloud_stack,
34-
appinspect_token,
35-
input_dto.stack_type)
36-
40+
try:
41+
# Request went through and completed, but may have returned a non-successful error code.
42+
# This likely includes a more verbose response describing the error
43+
res.raise_for_status()
44+
print(res.json())
45+
except Exception as e:
46+
try:
47+
error_text = res.json()
48+
except Exception as e:
49+
error_text = "No error text - request failed"
50+
formatted_error_text = pprint.pformat(error_text)
51+
print("While this may not be the cause of your error, ensure that the uid and appid of your Private App does not exist in Splunkbase\n"
52+
"ACS cannot deploy and app with the same uid or appid as one that exists in Splunkbase.")
53+
raise Exception(f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{formatted_error_text}")
3754

38-
55+
print(f"'{config.getPackageFilePath(include_version=False)}' successfully installed to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS!")

contentctl/contentctl.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from contentctl.actions.reporting import ReportingInputDto, Reporting
2020
from contentctl.actions.inspect import Inspect
2121
from contentctl.input.yml_reader import YmlReader
22+
from contentctl.actions.deploy_acs import Deploy
2223
from contentctl.actions.release_notes import ReleaseNotes
2324

2425
# def print_ascii_art():
@@ -95,8 +96,11 @@ def new_func(config:new):
9596

9697

9798
def deploy_acs_func(config:deploy_acs):
98-
#This is a bit challenging to get to work with the default values.
99-
raise Exception("deploy acs not yet implemented")
99+
print("Building and inspecting app...")
100+
token = inspect_func(config)
101+
print("App successfully built and inspected.")
102+
print("Deploying app...")
103+
Deploy().execute(config, token)
100104

101105
def test_common_func(config:test_common):
102106
if type(config) == test:

contentctl/objects/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ class StackType(StrEnum):
294294

295295

296296
class inspect(build):
297+
297298
splunk_api_username: str = Field(
298299
description="Splunk API username used for appinspect and Splunkbase downloads."
299300
)

contentctl/output/conf_output.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,26 @@ def writeHeaders(self) -> set[pathlib.Path]:
5757
pass
5858

5959

60-
def writeAppConf(self)->set[pathlib.Path]:
60+
61+
62+
def writeMiscellaneousAppFiles(self)->set[pathlib.Path]:
6163
written_files:set[pathlib.Path] = set()
62-
for output_app_path, template_name in [ ("default/app.conf", "app.conf.j2"),
63-
("default/content-version.conf", "content-version.j2")]:
64-
written_files.add(ConfWriter.writeConfFile(pathlib.Path(output_app_path),
65-
template_name,
66-
self.config,
67-
[self.config.app]))
64+
65+
written_files.add(ConfWriter.writeConfFile(pathlib.Path("default/content-version.conf"),
66+
"content-version.j2",
67+
self.config,
68+
[self.config.app]))
6869

6970
written_files.add(ConfWriter.writeManifestFile(pathlib.Path("app.manifest"),
7071
"app.manifest.j2",
7172
self.config,
7273
[self.config.app]))
74+
75+
written_files.add(ConfWriter.writeServerConf(self.config))
76+
77+
written_files.add(ConfWriter.writeAppConf(self.config))
78+
79+
7380
return written_files
7481

7582

contentctl/output/conf_writer.py

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,76 @@
1212
from contentctl.objects.config import build
1313
import xml.etree.ElementTree as ET
1414

15+
# This list is not exhaustive of all default conf files, but should be
16+
# sufficient for our purposes.
17+
DEFAULT_CONF_FILES = [
18+
"alert_actions.conf",
19+
"app.conf",
20+
"audit.conf",
21+
"authentication.conf",
22+
"authorize.conf",
23+
"bookmarks.conf",
24+
"checklist.conf",
25+
"collections.conf",
26+
"commands.conf",
27+
"conf.conf",
28+
"datamodels.conf",
29+
"datatypesbnf.conf",
30+
"default-mode.conf",
31+
"deploymentclient.conf",
32+
"distsearch.conf",
33+
"event_renderers.conf",
34+
"eventdiscoverer.conf",
35+
"eventtypes.conf",
36+
"federated.conf",
37+
"fields.conf",
38+
"global-banner.conf",
39+
"health.conf",
40+
"indexes.conf",
41+
"inputs.conf",
42+
"limits.conf",
43+
"literals.conf",
44+
"livetail.conf",
45+
"macros.conf",
46+
"messages.conf",
47+
"metric_alerts.conf",
48+
"metric_rollups.conf",
49+
"multikv.conf",
50+
"outputs.conf",
51+
"passwords.conf",
52+
"procmon-filters.conf",
53+
"props.conf",
54+
"pubsub.conf",
55+
"restmap.conf",
56+
"rolling_upgrade.conf",
57+
"savedsearches.conf",
58+
"searchbnf.conf",
59+
"segmenters.conf",
60+
"server.conf",
61+
"serverclass.conf",
62+
"serverclass.seed.xml.conf",
63+
"source-classifier.conf",
64+
"sourcetypes.conf",
65+
"tags.conf",
66+
"telemetry.conf",
67+
"times.conf",
68+
"transactiontypes.conf",
69+
"transforms.conf",
70+
"ui-prefs.conf",
71+
"ui-tour.conf",
72+
"user-prefs.conf",
73+
"user-seed.conf",
74+
"viewstates.conf",
75+
"visualizations.conf",
76+
"web-features.conf",
77+
"web.conf",
78+
"wmi.conf",
79+
"workflow_actions.conf",
80+
"workload_policy.conf",
81+
"workload_pools.conf",
82+
"workload_rules.conf",
83+
]
84+
1585
class ConfWriter():
1686

1787
@staticmethod
@@ -57,6 +127,52 @@ def writeConfFileHeader(app_output_path:pathlib.Path, config: build) -> pathlib.
57127
ConfWriter.validateConfFile(output_path)
58128
return output_path
59129

130+
@staticmethod
131+
def getCustomConfFileStems(config:build)->list[str]:
132+
# Get all the conf files in the default directory. We must make a reload.conf_file = simple key/value for them if
133+
# they are custom conf files
134+
default_path = config.getPackageDirectoryPath()/"default"
135+
conf_files = default_path.glob("*.conf")
136+
137+
custom_conf_file_stems = [conf_file.stem for conf_file in conf_files if conf_file.name not in DEFAULT_CONF_FILES]
138+
return sorted(custom_conf_file_stems)
139+
140+
@staticmethod
141+
def writeServerConf(config: build) -> pathlib.Path:
142+
app_output_path = pathlib.Path("default/server.conf")
143+
template_name = "server.conf.j2"
144+
145+
j2_env = ConfWriter.getJ2Environment()
146+
template = j2_env.get_template(template_name)
147+
148+
output = template.render(custom_conf_files=ConfWriter.getCustomConfFileStems(config))
149+
150+
output_path = config.getPackageDirectoryPath()/app_output_path
151+
output_path.parent.mkdir(parents=True, exist_ok=True)
152+
with open(output_path, 'a') as f:
153+
output = output.encode('utf-8', 'ignore').decode('utf-8')
154+
f.write(output)
155+
return output_path
156+
157+
158+
@staticmethod
159+
def writeAppConf(config: build) -> pathlib.Path:
160+
app_output_path = pathlib.Path("default/app.conf")
161+
template_name = "app.conf.j2"
162+
163+
j2_env = ConfWriter.getJ2Environment()
164+
template = j2_env.get_template(template_name)
165+
166+
output = template.render(custom_conf_files=ConfWriter.getCustomConfFileStems(config),
167+
app=config.app)
168+
169+
output_path = config.getPackageDirectoryPath()/app_output_path
170+
output_path.parent.mkdir(parents=True, exist_ok=True)
171+
with open(output_path, 'a') as f:
172+
output = output.encode('utf-8', 'ignore').decode('utf-8')
173+
f.write(output)
174+
return output_path
175+
60176
@staticmethod
61177
def writeManifestFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> pathlib.Path:
62178
j2_env = ConfWriter.getJ2Environment()
@@ -70,6 +186,7 @@ def writeManifestFile(app_output_path:pathlib.Path, template_name : str, config:
70186
output = output.encode('utf-8', 'ignore').decode('utf-8')
71187
f.write(output)
72188
return output_path
189+
73190

74191

75192
@staticmethod
@@ -218,8 +335,3 @@ def validateManifestFile(path:pathlib.Path):
218335
_ = json.load(manifestFile)
219336
except Exception as e:
220337
raise Exception(f"Failed to validate .manifest file {str(path)} (Note that .manifest files should contain only valid JSON-formatted data): {str(e)}")
221-
222-
223-
224-
225-

contentctl/output/templates/app.conf.j2

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,33 @@
44
is_configured = false
55
state = enabled
66
state_change_requires_restart = false
7-
build = {{ objects[0].build }}
7+
build = {{ app.build }}
88

99
[triggers]
10-
reload.analytic_stories = simple
11-
reload.usage_searches = simple
12-
reload.use_case_library = simple
13-
reload.correlationsearches = simple
14-
reload.analyticstories = simple
15-
reload.governance = simple
16-
reload.managed_configurations = simple
17-
reload.postprocess = simple
18-
reload.content-version = simple
19-
reload.es_investigations = simple
10+
{% for custom_conf_file in custom_conf_files%}
11+
reload.{{custom_conf_file}} = simple
12+
{% endfor %}
2013

2114
[launcher]
22-
author = {{ objects[0].author_company }}
23-
version = {{ objects[0].version }}
24-
description = {{ objects[0].description | escapeNewlines() }}
15+
author = {{ app.author_company }}
16+
version = {{ app.version }}
17+
description = {{ app.description | escapeNewlines() }}
2518

2619
[ui]
2720
is_visible = true
28-
label = {{ objects[0].title }}
21+
label = {{ app.title }}
2922

3023
[package]
31-
id = {{ objects[0].appid }}
24+
id = {{ app.appid }}
25+
{% if app.uid == 3449 %}
26+
check_for_updates = true
27+
{% else %}
28+
check_for_updates = false
29+
{% endif %}
30+
31+
[id]
32+
version = {{ app.version }}
33+
name = {{ app.appid }}
3234

3335

3436

contentctl/output/templates/app.manifest.j2

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"schemaVersion": "1.0.0",
2+
"schemaVersion": "1.0.0",
3+
"targetWorkloads": ["_search_heads"],
34
"info": {
45
"title": "{{ objects[0].title }}",
56
"id": {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[shclustering]
2+
{% for custom_conf_file in custom_conf_files%}
3+
conf_replication_include.{{custom_conf_file}} = true
4+
{% endfor %}

0 commit comments

Comments
 (0)