drove-cli 0.0.3__tar.gz → 0.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: drove-cli
3
+ Version: 0.0.5
4
+ Summary: Drove Command Line Interface
5
+ License: Apache-2.0
6
+ Keywords: container,docker,podman,distributed-systems,container-orchestrator
7
+ Author: Santanu Sinha
8
+ Author-email: santanu.sinha@gmail.com
9
+ Maintainer: Santanu Sinha
10
+ Maintainer-email: santanu.sinha@gmail.com
11
+ Requires-Python: >=3.7,<4.0
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: System :: Clustering
26
+ Classifier: Topic :: System :: Distributed Computing
27
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
28
+ Requires-Dist: tabulate (>=0.9.0,<0.10.0)
29
+ Requires-Dist: tenacity (>=8.2.3,<9.0.0)
30
+ Requires-Dist: urllib3 (>=2.0.7,<3.0.0)
31
+ Project-URL: Repository, https://github.com/PhonePe/drove-cli
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Drove CLI
35
+
36
+ Command-line interface for the [Drove Container Orchestrator](https://github.com/PhonePe/drove-orchestrator).
37
+
38
+ ## Installation
39
+
40
+ ### Using pip
41
+
42
+ ```bash
43
+ pip install drove-cli
44
+ ```
45
+
46
+ ### Using pip (virtual environment)
47
+
48
+ ```bash
49
+ python3 -m venv ~/.venvs/drove-cli
50
+ source ~/.venvs/drove-cli/bin/activate
51
+ pip install drove-cli
52
+ ```
53
+
54
+ To activate in a new shell:
55
+ ```bash
56
+ source ~/.venvs/drove-cli/bin/activate
57
+ ```
58
+
59
+ ### Using Docker
60
+
61
+ ```bash
62
+ docker pull ghcr.io/phonepe/drove-cli:latest
63
+ ```
64
+
65
+ Create a wrapper script for convenience:
66
+
67
+ ```bash
68
+ cat > ~/bin/drove << 'EOF'
69
+ #!/bin/sh
70
+ docker run --rm -it --network host \
71
+ -v ${HOME}/.drove:/root/.drove:ro \
72
+ ghcr.io/phonepe/drove-cli:latest "$@"
73
+ EOF
74
+ chmod +x ~/bin/drove
75
+ ```
76
+
77
+ ## Configuration
78
+
79
+ Create `~/.drove` with your cluster configuration:
80
+
81
+ ```ini
82
+ [DEFAULT]
83
+ stage_token = <your-stage-token>
84
+ prod_token = <your-prod-token>
85
+
86
+ [local]
87
+ endpoint = http://localhost:10000
88
+ username = admin
89
+ password = admin
90
+
91
+ [stage]
92
+ endpoint = https://drove.stage.example.com
93
+ auth_header = %(stage_token)s
94
+
95
+ [prod]
96
+ endpoint = https://drove.prod.example.com
97
+ auth_header = %(prod_token)s
98
+ ```
99
+
100
+ ## Quick Start
101
+
102
+ ```bash
103
+ # Verify connection
104
+ drove -c prod cluster ping
105
+
106
+ # View cluster status
107
+ drove -c prod cluster summary
108
+
109
+ # List applications
110
+ drove -c prod apps list
111
+
112
+ # Get application info
113
+ drove -c prod apps info <app-name>
114
+ ```
115
+
116
+ ## Commands
117
+
118
+ | Command | Description |
119
+ |---------|-----------------------------------------------------------------------|
120
+ | `appinstances` | Application instance operations |
121
+ | `apps` | Application lifecycle Management (list, info, deploy, scale, suspend) |
122
+ | `cluster` | Cluster operations (ping, summary, leader, maintenance) |
123
+ | `config` | CLI configuration management |
124
+ | `describe` | Show detailed information about a resource |
125
+ | `executor` | Executor management |
126
+ | `localservices` | Local service management |
127
+ | `lsinstances` | Local service instance operations |
128
+ | `tasks` | One-off task execution |
129
+
130
+ Use `drove -h` or `drove <command> -h` for detailed help.
131
+
132
+ ## Global Options
133
+
134
+ ```
135
+ -f, --file FILE Configuration file (default: ~/.drove)
136
+ -c, --cluster CLUSTER Cluster name from config file
137
+ -e, --endpoint URL Drove endpoint URL
138
+ -t, --auth-header HDR Authorization header value
139
+ -u, --username USER Cluster username
140
+ -p, --password PASS Cluster password
141
+ -i, --insecure Skip SSL verification
142
+ -d, --debug Print error details
143
+ ```
144
+
145
+ ## Documentation
146
+
147
+ Full documentation is available at **[github.com/PhonePe/drove-orchestrator](https://github.com/PhonePe/drove-orchestrator)**
148
+
149
+ ## License
150
+
151
+ © 2024 Santanu Sinha | Apache 2.0
152
+
@@ -0,0 +1,118 @@
1
+ # Drove CLI
2
+
3
+ Command-line interface for the [Drove Container Orchestrator](https://github.com/PhonePe/drove-orchestrator).
4
+
5
+ ## Installation
6
+
7
+ ### Using pip
8
+
9
+ ```bash
10
+ pip install drove-cli
11
+ ```
12
+
13
+ ### Using pip (virtual environment)
14
+
15
+ ```bash
16
+ python3 -m venv ~/.venvs/drove-cli
17
+ source ~/.venvs/drove-cli/bin/activate
18
+ pip install drove-cli
19
+ ```
20
+
21
+ To activate in a new shell:
22
+ ```bash
23
+ source ~/.venvs/drove-cli/bin/activate
24
+ ```
25
+
26
+ ### Using Docker
27
+
28
+ ```bash
29
+ docker pull ghcr.io/phonepe/drove-cli:latest
30
+ ```
31
+
32
+ Create a wrapper script for convenience:
33
+
34
+ ```bash
35
+ cat > ~/bin/drove << 'EOF'
36
+ #!/bin/sh
37
+ docker run --rm -it --network host \
38
+ -v ${HOME}/.drove:/root/.drove:ro \
39
+ ghcr.io/phonepe/drove-cli:latest "$@"
40
+ EOF
41
+ chmod +x ~/bin/drove
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ Create `~/.drove` with your cluster configuration:
47
+
48
+ ```ini
49
+ [DEFAULT]
50
+ stage_token = <your-stage-token>
51
+ prod_token = <your-prod-token>
52
+
53
+ [local]
54
+ endpoint = http://localhost:10000
55
+ username = admin
56
+ password = admin
57
+
58
+ [stage]
59
+ endpoint = https://drove.stage.example.com
60
+ auth_header = %(stage_token)s
61
+
62
+ [prod]
63
+ endpoint = https://drove.prod.example.com
64
+ auth_header = %(prod_token)s
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ ```bash
70
+ # Verify connection
71
+ drove -c prod cluster ping
72
+
73
+ # View cluster status
74
+ drove -c prod cluster summary
75
+
76
+ # List applications
77
+ drove -c prod apps list
78
+
79
+ # Get application info
80
+ drove -c prod apps info <app-name>
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ | Command | Description |
86
+ |---------|-----------------------------------------------------------------------|
87
+ | `appinstances` | Application instance operations |
88
+ | `apps` | Application lifecycle Management (list, info, deploy, scale, suspend) |
89
+ | `cluster` | Cluster operations (ping, summary, leader, maintenance) |
90
+ | `config` | CLI configuration management |
91
+ | `describe` | Show detailed information about a resource |
92
+ | `executor` | Executor management |
93
+ | `localservices` | Local service management |
94
+ | `lsinstances` | Local service instance operations |
95
+ | `tasks` | One-off task execution |
96
+
97
+ Use `drove -h` or `drove <command> -h` for detailed help.
98
+
99
+ ## Global Options
100
+
101
+ ```
102
+ -f, --file FILE Configuration file (default: ~/.drove)
103
+ -c, --cluster CLUSTER Cluster name from config file
104
+ -e, --endpoint URL Drove endpoint URL
105
+ -t, --auth-header HDR Authorization header value
106
+ -u, --username USER Cluster username
107
+ -p, --password PASS Cluster password
108
+ -i, --insecure Skip SSL verification
109
+ -d, --debug Print error details
110
+ ```
111
+
112
+ ## Documentation
113
+
114
+ Full documentation is available at **[github.com/PhonePe/drove-orchestrator](https://github.com/PhonePe/drove-orchestrator)**
115
+
116
+ ## License
117
+
118
+ © 2024 Santanu Sinha | Apache 2.0
@@ -173,12 +173,19 @@ def build_drove_client(drove_client: DroveClient, args: SimpleNamespace):
173
173
  with open(config_file) as stream:
174
174
  config_parser.read_string(stream.read())
175
175
  drove_config = config_parser['DEFAULT']
176
- if args.cluster is not None:
177
- if args.cluster in config_parser:
178
- drove_config = config_parser[args.cluster]
176
+
177
+ # Determine which cluster to use (priority: -c flag > current_cluster in config > DEFAULT)
178
+ cluster_to_use = args.cluster
179
+ if cluster_to_use is None:
180
+ cluster_to_use = config_parser.defaults().get('current_cluster')
181
+
182
+ if cluster_to_use is not None and cluster_to_use != 'DEFAULT':
183
+ if cluster_to_use in config_parser:
184
+ drove_config = config_parser[cluster_to_use]
179
185
  else:
180
- print("error: No cluster definition found for {cluster} in config {config_file}".format(config_file=config_file, cluster=args.cluster))
186
+ print("error: No cluster definition found for {cluster} in config {config_file}".format(config_file=config_file, cluster=cluster_to_use))
181
187
  return None
188
+
182
189
  endpoint = drove_config.get("endpoint")
183
190
  username = drove_config.get("username")
184
191
  password = drove_config.get("password")
@@ -0,0 +1,314 @@
1
+ """
2
+ Config plugin for drove-cli - cluster configuration management.
3
+
4
+ This plugin adds 'drove config' commands for managing clusters, largely inspired by kubectl context management.
5
+ - drove config current-cluster : Show current default cluster
6
+ - drove config get-clusters : List all configured clusters
7
+ - drove config use-cluster <name> : Set default cluster
8
+ - drove config view : Show full config file
9
+ - drove config init : Initialize a new config file
10
+ - drove config add-cluster <name> : Add a new cluster to config
11
+ - drove config delete-cluster <name> : Remove a cluster from config
12
+
13
+
14
+ Usage:
15
+ drove config use-cluster local
16
+ """
17
+
18
+ import argparse
19
+ import configparser
20
+ import os
21
+ import plugins
22
+
23
+ from pathlib import Path
24
+ from types import SimpleNamespace
25
+
26
+
27
+ def get_config_file_path() -> str:
28
+ return str(Path.home()) + "/.drove"
29
+
30
+
31
+ def read_config() -> configparser.ConfigParser:
32
+ config_file = get_config_file_path()
33
+ config = configparser.ConfigParser()
34
+
35
+ if os.path.isfile(config_file) and os.access(config_file, os.R_OK):
36
+ with open(config_file) as stream:
37
+ config.read_string(stream.read())
38
+
39
+ return config
40
+
41
+
42
+ def write_config(config: configparser.ConfigParser) -> None:
43
+ config_file = get_config_file_path()
44
+ with open(config_file, 'w') as f:
45
+ config.write(f)
46
+
47
+
48
+ def get_current_cluster() -> str:
49
+ config = read_config()
50
+ return config.defaults().get('current_cluster')
51
+
52
+
53
+ def set_current_cluster(cluster_name: str) -> None:
54
+ config = read_config()
55
+ config.set('DEFAULT', 'current_cluster', cluster_name)
56
+ write_config(config)
57
+
58
+
59
+ def get_clusters_from_config(config: configparser.ConfigParser) -> list:
60
+ clusters = []
61
+ for section in config.sections():
62
+ endpoint = config[section].get('endpoint', '')
63
+ if endpoint:
64
+ clusters.append({
65
+ 'name': section,
66
+ 'endpoint': endpoint,
67
+ 'has_auth': bool(config[section].get('username') or config[section].get('auth_header')),
68
+ 'insecure': config[section].get('insecure', 'false').lower() == 'true'
69
+ })
70
+
71
+ if config.defaults().get('endpoint'):
72
+ clusters.insert(0, {
73
+ 'name': 'DEFAULT',
74
+ 'endpoint': config.defaults().get('endpoint'),
75
+ 'has_auth': bool(config.defaults().get('username') or config.defaults().get('auth_header')),
76
+ 'insecure': config.defaults().get('insecure', 'false').lower() == 'true'
77
+ })
78
+
79
+ return clusters
80
+
81
+
82
+ class Config(plugins.DrovePlugin):
83
+ """Plugin for config/context management commands."""
84
+
85
+ def __init__(self) -> None:
86
+ pass
87
+
88
+ def populate_options(self, drove_client, subparser: argparse.ArgumentParser):
89
+ parser = subparser.add_parser("config", help="Manage drove cluster configurations")
90
+ commands = parser.add_subparsers(help="Available config commands")
91
+
92
+ # get-clusters: List all available clusters
93
+ sub_parser = commands.add_parser("get-clusters", help="List all configured clusters")
94
+ sub_parser.set_defaults(func=self.get_clusters)
95
+
96
+ # current-cluster: Show current default
97
+ sub_parser = commands.add_parser("current-cluster", help="Show current default cluster")
98
+ sub_parser.set_defaults(func=self.current_cluster)
99
+
100
+ # use-cluster: Set default cluster
101
+ sub_parser = commands.add_parser("use-cluster", help="Set default cluster")
102
+ sub_parser.add_argument("cluster_name", metavar="cluster-name", help="Cluster name to use as default")
103
+ sub_parser.set_defaults(func=self.use_cluster)
104
+
105
+ # view: Show full config
106
+ sub_parser = commands.add_parser("view", help="Display the full configuration file")
107
+ sub_parser.add_argument("--raw", "-r", help="Show raw config file content", action="store_true")
108
+ sub_parser.set_defaults(func=self.view_config)
109
+
110
+ # init: Initialize config file
111
+ sub_parser = commands.add_parser("init", help="Initialize a new ~/.drove config file")
112
+ sub_parser.add_argument("--endpoint", "-e", help="Drove endpoint URL", required=True)
113
+ sub_parser.add_argument("--name", "-n", help="Cluster name", default="default")
114
+ sub_parser.add_argument("--username", "-u", help="Username for basic auth")
115
+ sub_parser.add_argument("--password", "-p", help="Password for basic auth")
116
+ sub_parser.add_argument("--auth-header", "-t", dest="auth_header", help="Authorization header value")
117
+ sub_parser.add_argument("--insecure", "-i", help="Skip SSL verification", action="store_true")
118
+ sub_parser.set_defaults(func=self.init_config)
119
+
120
+ # add-cluster: Add a new cluster to config
121
+ sub_parser = commands.add_parser("add-cluster", help="Add a new cluster to config")
122
+ sub_parser.add_argument("cluster_name", metavar="cluster-name", help="Name for this cluster")
123
+ sub_parser.add_argument("--endpoint", "-e", help="Drove endpoint URL", required=True)
124
+ sub_parser.add_argument("--username", "-u", help="Username for basic auth")
125
+ sub_parser.add_argument("--password", "-p", help="Password for basic auth")
126
+ sub_parser.add_argument("--auth-header", "-t", dest="auth_header", help="Authorization header value")
127
+ sub_parser.add_argument("--insecure", "-i", help="Skip SSL verification", action="store_true")
128
+ sub_parser.set_defaults(func=self.add_cluster)
129
+
130
+ # delete-cluster: Remove a cluster from config
131
+ sub_parser = commands.add_parser("delete-cluster", help="Remove a cluster from config")
132
+ sub_parser.add_argument("cluster_name", metavar="cluster-name", help="Cluster name to remove")
133
+ sub_parser.set_defaults(func=self.delete_cluster)
134
+
135
+ super().populate_options(drove_client, parser)
136
+
137
+ def get_clusters(self, options: SimpleNamespace):
138
+ config = read_config()
139
+ clusters = get_clusters_from_config(config)
140
+ current = get_current_cluster()
141
+
142
+ if not clusters:
143
+ print("No clusters configured. Run 'drove config init' to set up.")
144
+ return
145
+
146
+ print(f"{'CURRENT':<10} {'NAME':<20} {'ENDPOINT':<50} {'AUTH':<6} {'INSECURE':<8}")
147
+ print("-" * 95)
148
+
149
+ for cluster in clusters:
150
+ marker = "*" if cluster['name'] == current else ""
151
+ auth = "yes" if cluster['has_auth'] else "no"
152
+ insecure = "yes" if cluster['insecure'] else "no"
153
+ print(f"{marker:<10} {cluster['name']:<20} {cluster['endpoint']:<50} {auth:<6} {insecure:<8}")
154
+
155
+ if current:
156
+ print(f"\nCurrent cluster: {current}")
157
+
158
+ def current_cluster(self, options: SimpleNamespace):
159
+ current = get_current_cluster()
160
+ if current:
161
+ print(current)
162
+ else:
163
+ print("No current cluster set. Use 'drove config use-cluster <name>' to set one.")
164
+
165
+ def use_cluster(self, options: SimpleNamespace):
166
+ config = read_config()
167
+ cluster_name = options.cluster_name
168
+
169
+ # Validate cluster exists
170
+ valid_names = [s for s in config.sections()]
171
+ if cluster_name == 'DEFAULT':
172
+ if config.defaults().get('endpoint'):
173
+ valid_names.append('DEFAULT')
174
+
175
+ if cluster_name not in valid_names and cluster_name != 'DEFAULT':
176
+ print(f"Error: cluster '{cluster_name}' not found in config")
177
+ print(f"Available clusters: {', '.join(valid_names) if valid_names else 'none'}")
178
+ return
179
+
180
+ set_current_cluster(cluster_name)
181
+ print(f"Switched to cluster \"{cluster_name}\".")
182
+
183
+ def view_config(self, options: SimpleNamespace):
184
+ config_file = get_config_file_path()
185
+
186
+ if not os.path.isfile(config_file):
187
+ print(f"Config file not found: {config_file}")
188
+ print("Run 'drove config init' to create one.")
189
+ return
190
+
191
+ if options.raw:
192
+ with open(config_file, 'r') as f:
193
+ print(f.read())
194
+ else:
195
+ config = read_config()
196
+ current = get_current_cluster()
197
+
198
+ print(f"Config file: {config_file}")
199
+ print(f"Current cluster: {current or '(not set)'}")
200
+ print()
201
+
202
+ for section in ['DEFAULT'] + config.sections():
203
+ if section == 'DEFAULT':
204
+ items = dict(config.defaults())
205
+ else:
206
+ items = dict(config[section])
207
+
208
+ if not items.get('endpoint'):
209
+ continue
210
+
211
+ marker = " (current)" if section == current else ""
212
+ print(f"[{section}]{marker}")
213
+ for key, value in items.items():
214
+ # Mask sensitive values
215
+ if key in ('password', 'auth_header') and value:
216
+ display_value = value[:4] + "****" + value[-4:] if len(value) > 8 else "****"
217
+ else:
218
+ display_value = value
219
+ print(f" {key} = {display_value}")
220
+ print()
221
+
222
+ def init_config(self, options: SimpleNamespace):
223
+ config_file = get_config_file_path()
224
+
225
+ if os.path.isfile(config_file):
226
+ print(f"Config file already exists: {config_file}")
227
+ print("Use 'drove config add-context' to add more clusters.")
228
+ return
229
+
230
+ config = configparser.ConfigParser()
231
+ section = options.name if options.name != 'default' else 'DEFAULT'
232
+
233
+ if section != 'DEFAULT':
234
+ config.add_section(section)
235
+ config[section]
236
+ else:
237
+ config.defaults()
238
+
239
+ if section == 'DEFAULT':
240
+ config.set('DEFAULT', 'endpoint', options.endpoint)
241
+ if options.username:
242
+ config.set('DEFAULT', 'username', options.username)
243
+ if options.password:
244
+ config.set('DEFAULT', 'password', options.password)
245
+ if options.auth_header:
246
+ config.set('DEFAULT', 'auth_header', options.auth_header)
247
+ if options.insecure:
248
+ config.set('DEFAULT', 'insecure', 'true')
249
+ else:
250
+ config[section]['endpoint'] = options.endpoint
251
+ if options.username:
252
+ config[section]['username'] = options.username
253
+ if options.password:
254
+ config[section]['password'] = options.password
255
+ if options.auth_header:
256
+ config[section]['auth_header'] = options.auth_header
257
+ if options.insecure:
258
+ config[section]['insecure'] = 'true'
259
+
260
+ with open(config_file, 'w') as f:
261
+ config.write(f)
262
+
263
+ set_current_cluster(section)
264
+
265
+ print(f"Config initialized at: {config_file}")
266
+ print(f"Current cluster set to: {section}")
267
+
268
+ def add_cluster(self, options: SimpleNamespace):
269
+ """Add a new cluster."""
270
+ config = read_config()
271
+ cluster_name = options.cluster_name
272
+
273
+ if cluster_name in config.sections():
274
+ print(f"Error: cluster '{cluster_name}' already exists")
275
+ return
276
+
277
+ config.add_section(cluster_name)
278
+ config[cluster_name]['endpoint'] = options.endpoint
279
+ if options.username:
280
+ config[cluster_name]['username'] = options.username
281
+ if options.password:
282
+ config[cluster_name]['password'] = options.password
283
+ if options.auth_header:
284
+ config[cluster_name]['auth_header'] = options.auth_header
285
+ if options.insecure:
286
+ config[cluster_name]['insecure'] = 'true'
287
+
288
+ config_file = get_config_file_path()
289
+ with open(config_file, 'w') as f:
290
+ config.write(f)
291
+
292
+ print(f"Cluster '{cluster_name}' added to {config_file}")
293
+
294
+ def delete_cluster(self, options: SimpleNamespace):
295
+ config = read_config()
296
+ cluster_name = options.cluster_name
297
+
298
+ if cluster_name not in config.sections():
299
+ print(f"Error: cluster '{cluster_name}' not found")
300
+ return
301
+
302
+ config.remove_section(cluster_name)
303
+
304
+ config_file = get_config_file_path()
305
+ with open(config_file, 'w') as f:
306
+ config.write(f)
307
+
308
+ current = get_current_cluster()
309
+ if current == cluster_name:
310
+ config.remove_option('DEFAULT', 'current_cluster')
311
+ write_config(config)
312
+ print(f"Cluster '{cluster_name}' deleted and was current cluster (now unset)")
313
+ else:
314
+ print(f"Cluster '{cluster_name}' deleted from {config_file}")