Skip to content

Commit

Permalink
Merge pull request #100 from duplocloud/config-from-files
Browse files Browse the repository at this point in the history
configmaps and secrets from files and literals
  • Loading branch information
kferrone authored Aug 27, 2024
2 parents 18b0be0 + a94359c commit d32c1f2
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Include generated Github release notes in the release description
- Install instructions in the docs
- Cleaned up pipeline and added test reporting into the PRs
- Configmaps and secrets can be created with data values `--from-file` and `--from-literal`. The result can be displayed with `--dry-run`. Both are a key=value pair but files can simply default the key to the filename.

### Fixed

- Fixed update_pod_label subcommand functionality for service.
- fixed many little issues with the docs like misspelled args, unneeded extra ones, and even missing types
- discovered why Args were not renaming based on the function arg.

## [0.2.33] - 2024-08-12

Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ RUN pip install --no-cache-dir ./dist/*.whl && \

# Set the entrypoint command for the container
ENTRYPOINT ["duploctl"]

CMD [ "version" ]
41 changes: 10 additions & 31 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,37 @@ services:

duploctl: &base
image: &image duplocloud/duploctl:latest
build: &build
build:
target: runner
args:
args: &args
PY_VERSION: ${PY_VERSION:-3.12}
tags:
- *image
- duplocloud/duploctl:${GIT_SHA:-latest}
- duplocloud/duploctl:${GIT_REF:-latest}
- duplocloud/duploctl:${GIT_TAG:-latest}
platforms: &platforms
- linux/amd64
- linux/arm64
x-bake: &bake
platforms: *platforms
x-bake:
platforms: &platforms
- linux/amd64
- linux/arm64
cache-to: type=gha,scope=runner,mode=max
cache-from: type=gha,scope=runner
container_name: duploctl
environment: &environment
DUPLO_HOST: ${DUPLO_HOST:-}
DUPLO_TOKEN: ${DUPLO_TOKEN:-}
DUPLO_TENANT: ${DUPLO_TENANT:-}
# command:
# - version
DUPLO_OUTPUT: yaml

duploctl-bin:
<<: *base
container_name: duploctl-bin
image: &binImage duplocloud/duploctl:bin
image: duplocloud/duploctl:bin
build:
<<: *build
target: bin
tags:
- *binImage
args: *args
x-bake:
<<: *bake
platforms: *platforms
output: type=local,dest=./dist
cache-to: type=gha,scope=installer,mode=max
cache-from: type=gha,scope=installer

duploctl-dev:
image: duplocloud/duploctl:dev
build:
target: dev
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- .:/workspaces:cached

# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined

# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
10 changes: 9 additions & 1 deletion scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@

mkdir -p config dist dist/docs

pip install --editable .[build,test,aws,docs]
pip install --editable '.[build,test,aws,docs]'

# for each folder in the plugins directory
for plugin in plugins/*; do
# install only if the folder is a directory and a pyproject.toml file exists
if [ -d "$plugin" ] && [ -f "$plugin/pyproject.toml" ]; then
pip install --editable "$plugin"
fi
done
61 changes: 60 additions & 1 deletion src/duplo_resource/configmap.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,69 @@
from duplocloud.client import DuploClient
from duplocloud.errors import DuploError
from duplocloud.resource import DuploTenantResourceV3
from duplocloud.commander import Resource
from duplocloud.commander import Command, Resource
import duplocloud.args as args

@Resource("configmap")
class DuploConfigMap(DuploTenantResourceV3):

def __init__(self, duplo: DuploClient):
super().__init__(duplo, "k8s/configmap")

@Command()
def create(self,
name: args.NAME=None,
body: args.BODY=None,
data: args.DATAMAP=None,
dryrun: args.DRYRUN=False,
wait: args.WAIT=False) -> dict:
"""Create a Configmap resource.
Usage: CLI Usage
```sh
duploctl configmap create -f 'configmap.yaml'
```
Contents of the `configmap.yaml` file
```yaml
--8<-- "src/tests/data/configmap.yaml"
```
Example: One liner example
```sh
echo \"\"\"
--8<-- "src/tests/data/configmap.yaml"
\"\"\" | duploctl configmap create -f -
```
Args:
name: The name to set the configmap to.
body: The resource to create.
data: The data to add to the configmap.
dryrun: Do not submit any changes to the server.
wait: Wait for the resource to be created.
Returns:
message: Success message.
Raises:
DuploError: If the resource could not be created.
"""
if not name and not body:
raise DuploError("Name is required when body is not provided")
if not body:
body = {}
# make sure the body has a metadata key
if 'metadata' not in body:
body['metadata'] = {}
# also make sure the data key is present
if 'data' not in body:
body['data'] = {}
if name:
body['metadata']['name'] = name
if data:
body['data'].update(data)
if dryrun:
return body
else:
return super().create(body, wait=wait)

28 changes: 27 additions & 1 deletion src/duplo_resource/secret.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from duplocloud import args
from duplocloud.client import DuploClient
from duplocloud.errors import DuploError
from duplocloud.resource import DuploTenantResourceV3
from duplocloud.commander import Resource
from duplocloud.commander import Command, Resource

@Resource("secret")
class DuploSecret(DuploTenantResourceV3):
Expand All @@ -10,3 +12,27 @@ def __init__(self, duplo: DuploClient):

def name_from_body(self, body):
return body["SecretName"]

@Command()
def create(self,
name: args.NAME=None,
body: args.BODY=None,
data: args.DATAMAP=None,
dryrun: args.DRYRUN=False,
wait: args.WAIT=False) -> dict:
"""Create a Secret"""
if not name and not body:
raise DuploError("Name is required when body is not provided")
if not body:
body = {}
# also make sure the data key is present
if 'SecretData' not in body:
body['SecretData'] = {}
if name:
body['SecretName'] = name
if data:
body['SecretData'].update(data)
if dryrun:
return body
else:
return super().create(body, wait=wait)
15 changes: 14 additions & 1 deletion src/duplocloud/args.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import argparse
import logging
from .argtype import Arg, YamlAction, JsonPatchAction
from .argtype import Arg, YamlAction, JsonPatchAction, DataMapAction
from .commander import available_resources, available_formats, VERSION

# the global args for the CLI
Expand Down Expand Up @@ -72,6 +72,19 @@
help='A file to read the input from',
type=argparse.FileType('r'),
action=YamlAction)
"""File Body
This is the file path to a file with the specified resources body within. Each Resource will have it's own schema for the body. This is a yaml/json file that will be parsed and used as the body of the request. View the docs for each individual resource to see the schema for the body.
"""

DATAMAP = Arg("fromfile","--from-file", "--from-literal",
help='A file or literal value to add to the data map',
action=DataMapAction)

DRYRUN = Arg("dryrun", "--dry-run",
help='Do not submit any changes to the server',
type=bool,
action='store_true')

ARN = Arg("aws-arn", "--arn",
help='The aws arn',
Expand Down
37 changes: 36 additions & 1 deletion src/duplocloud/argtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def flags(self):
if self.positional:
# return the name
return [self.__name__]
return [*self.__flags, f"--{self.__name__}"]
return [f"--{self.__name__}", *self.__flags]
@property
def positional(self):
return len(self.__flags) == 0
Expand Down Expand Up @@ -145,3 +145,38 @@ def validate_value(v):
elif op in ["copy", "move"]:
patch = {"op": op, "from": key, "path": validate_key(value[1])}
super().__call__(parser, namespace, patch, option_string)

class DataMapAction(argparse.Action):
def __init__(self, option_strings, dest, nargs='+', **kwargs):
super(DataMapAction, self).__init__(option_strings, dest, nargs=nargs, **kwargs)
self.__filetype = argparse.FileType()
def __call__(self, parser, namespace, values, option_string=None):
key = None
value = None
items = getattr(namespace, self.dest, None)
data = items if items else {}
if option_string == "--from-file":
key, value = self.__file_value(values[0])
elif option_string == "--from-literal":
key, value = self.__literal_value(values[0])
data[key] = value
setattr(namespace, self.dest, data)

def __file_value(self, string):
key = None
fpath = None
parts = string.split("=", 1)
if len(parts) == 1:
fpath = parts[0]
key = "stdin" if fpath == "-" else os.path.basename(fpath)
elif len(parts) == 2:
key, fpath = parts
f = self.__filetype(fpath)
value = f.read().strip()
return [key, value]

def __literal_value(self, string):
parts = string.split("=", 1)
if len(parts) != 2:
raise argparse.ArgumentTypeError("Literal values must be in the format key=value")
return parts
1 change: 1 addition & 0 deletions src/tests/files/password.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
verysecretpassword
24 changes: 23 additions & 1 deletion src/tests/test_commander.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import pathlib
import pytest
# import unittest
import argparse
from duplocloud.commander import schema, resources, Command, get_parser, aliased_method, extract_args, available_resources, load_resource
from duplocloud.argtype import Arg
from duplocloud.argtype import Arg, DataMapAction
from duplocloud.errors import DuploError
# from duplo_resource.service import DuploService
# from duplocloud.resource import DuploResource

dir = pathlib.Path(__file__).parent.resolve()

NAME = Arg("name",
help='A test name arg')

Expand Down Expand Up @@ -96,3 +99,22 @@ def test_arg_type():
def test_aliased_command():
method = aliased_method(SomeResource, "test")
assert method == "tester"

@pytest.mark.unit
def test_datamap_action():
fname = "password.txt"
password = "verysecretpassword"
fpath = f"{dir}/files/{fname}"
p = argparse.ArgumentParser()
p.add_argument("--from-file", "--from-literal", dest="data", action=DataMapAction)
args = [
"--from-file", fpath,
"--from-file", f"renamed={fpath}",
"--from-literal", "foo=bar"
]
ns = p.parse_args(args)
assert ns.data == {
"password.txt": password,
"renamed": password,
"foo": "bar"
}

0 comments on commit d32c1f2

Please sign in to comment.